@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-tools.ts
CHANGED
|
@@ -77,9 +77,10 @@
|
|
|
77
77
|
//
|
|
78
78
|
// `anno_batch_execute` IS THE ONE SANCTIONED NESTED-ARGUMENT VERB ON THIS
|
|
79
79
|
// SURFACE, AND NO SECOND MAY JOIN IT. A meta-tool that takes an arbitrary tool
|
|
80
|
-
// name inside its own arguments is precisely the
|
|
81
|
-
//
|
|
82
|
-
// name
|
|
80
|
+
// name inside its own arguments is precisely the confused-deputy shape a
|
|
81
|
+
// generic meta-tool surface this project closed once had, with an
|
|
82
|
+
// outer-name-only refusal array: the outer name passes the gate while the
|
|
83
|
+
// inner name never sees it. This one verb earns the exception by being the only
|
|
83
84
|
// route to the multi-edit pass an annotation run actually performs, and it
|
|
84
85
|
// pays for it with `assertAnnoBatch()` below -- a recursive, DEPTH-CAPPED
|
|
85
86
|
// pre-validator that refuses the WHOLE batch, before any store is opened, if
|
|
@@ -134,19 +135,28 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
134
135
|
import { extname } from "node:path";
|
|
135
136
|
|
|
136
137
|
import {
|
|
138
|
+
addExcludedRange,
|
|
137
139
|
addScope,
|
|
138
140
|
applyEnumUsage,
|
|
141
|
+
applyWrite,
|
|
139
142
|
clearEnumUsage,
|
|
140
143
|
closeStore,
|
|
141
144
|
createProjectEnum,
|
|
142
145
|
currentRevision,
|
|
146
|
+
deleteExecObservationsForRun,
|
|
147
|
+
insertExecObservations,
|
|
143
148
|
listComments,
|
|
144
149
|
listEnumUsage,
|
|
150
|
+
listExcludedRanges,
|
|
151
|
+
listExecObservations,
|
|
145
152
|
listLabels,
|
|
153
|
+
listObservedRuns,
|
|
146
154
|
listProjectEnums,
|
|
147
155
|
listRanges,
|
|
148
156
|
listScopes,
|
|
157
|
+
listXrefs,
|
|
149
158
|
openStore,
|
|
159
|
+
removeExcludedRange,
|
|
150
160
|
removeScope,
|
|
151
161
|
setComment,
|
|
152
162
|
setDataType,
|
|
@@ -158,6 +168,7 @@ import {
|
|
|
158
168
|
AnnoRevisionArgumentError,
|
|
159
169
|
AnnoStoreError,
|
|
160
170
|
AnnoStorePathError,
|
|
171
|
+
AnnoStoreStaleRevisionError,
|
|
161
172
|
assertCommentText,
|
|
162
173
|
assertCommentType,
|
|
163
174
|
assertDataType,
|
|
@@ -166,13 +177,39 @@ import {
|
|
|
166
177
|
assertLegalLabel,
|
|
167
178
|
assertRangeShape,
|
|
168
179
|
parseStoreAddress,
|
|
180
|
+
parseVariantKey,
|
|
169
181
|
storePathWithinWorkspace,
|
|
170
182
|
} from "./anno-types.ts";
|
|
171
|
-
import type { AnnoStoreErrorOptions, CommentRow, LabelRow } from "./anno-types.ts";
|
|
183
|
+
import type { AnnoStoreErrorOptions, CommentRow, EnumUsageRow, LabelRow, ProjectEnumRow } from "./anno-types.ts";
|
|
172
184
|
import { crossReferencesTo, searchAnnotations } from "./anno-derive.ts";
|
|
173
185
|
import { composeAddressDetails } from "./anno-details.ts";
|
|
174
186
|
import { decode } from "./disasm-decoder.ts";
|
|
187
|
+
import type { Instruction } from "./disasm-decoder.ts";
|
|
175
188
|
import { render } from "./disasm-renderer.ts";
|
|
189
|
+
// D-16's SECOND renderer (plan 45-05): `anno-export-asm.ts` carries the
|
|
190
|
+
// proof (a real-ACME byte-diff oracle), this file carries the readability --
|
|
191
|
+
// both call decomposeRegisterValue(), the ONE owning decoder, and NEITHER
|
|
192
|
+
// decodes a bit itself. `REGISTER_ENUM_NAME_RE` below is deliberately a
|
|
193
|
+
// SEPARATE, small predicate from `anno-export-asm.ts`'s own copy: D-16 names
|
|
194
|
+
// two renderers, each owning its own substitution glue, and only the decoder
|
|
195
|
+
// itself is shared. `hasRegBitsEntry()` is likewise shared (45-REVIEW CR-01,
|
|
196
|
+
// fixed 2026-09-11): both renderers gate the decoder attempt on TABLE
|
|
197
|
+
// MEMBERSHIP, not name shape alone, via this one exported predicate -- a
|
|
198
|
+
// second, locally-derived membership test would be exactly the kind of
|
|
199
|
+
// "two answers to one question" this file's own header elsewhere refuses.
|
|
200
|
+
import { decomposeRegisterValue, hasRegBitsEntry, type RegisterDecomposition } from "./anno-enum-gen.ts";
|
|
201
|
+
import { importGhidraExport } from "./anno-import.ts";
|
|
202
|
+
import type { ConstWriteFact } from "./anno-import.ts";
|
|
203
|
+
import { runMemmapJoin } from "./anno-join.ts";
|
|
204
|
+
import { accessMapRanges, parseAccessMap } from "./textmon-memmap.ts";
|
|
205
|
+
import { ingestAccessMap, runIdentityFrom, type IngestRunIdentity } from "./evid-ingest.ts";
|
|
206
|
+
import { reconcileObservedExecution } from "./evid-reconcile.ts";
|
|
207
|
+
// The pure, read-only movement-hazard report. Declares its own
|
|
208
|
+
// input shapes and never reads a store, a file or a tool on its own behalf --
|
|
209
|
+
// the SAME caller-fetches-everything split `anno-coverage.ts`'s own header
|
|
210
|
+
// states for the coverage instrument, and exactly why the store re-point
|
|
211
|
+
// below is a CALLER-side change and nothing more.
|
|
212
|
+
import { buildHazardReport } from "./anno-hazard-report.ts";
|
|
176
213
|
import { flatImageOrigin, parsePrg } from "./prg-image.ts";
|
|
177
214
|
import { repoRoot } from "./repo-root.ts";
|
|
178
215
|
|
|
@@ -322,6 +359,22 @@ function assertMaxResults(name: string, args: unknown, batchIndex?: number): num
|
|
|
322
359
|
return raw as number;
|
|
323
360
|
}
|
|
324
361
|
|
|
362
|
+
/** `anno_evid_disagreements`'s own OPTIONAL `max_results` (plan 43-06).
|
|
363
|
+
* Unlike every other list-returning verb (`assertMaxResults` above, REQUIRED
|
|
364
|
+
* with no default), an unbounded disagreement report is the ordinary case: a
|
|
365
|
+
* sound store often disagrees nowhere at all, and forcing a ceiling on a
|
|
366
|
+
* legitimately small or empty answer would buy nothing. When SUPPLIED, the
|
|
367
|
+
* bound and refusal wording are the SAME as `assertMaxResults`'s -- this is
|
|
368
|
+
* not a second, looser rule, only an optional one. */
|
|
369
|
+
function assertOptionalMaxResults(name: string, args: unknown, batchIndex?: number): number | undefined {
|
|
370
|
+
const raw = argBag(args).max_results;
|
|
371
|
+
if (raw === undefined) return undefined;
|
|
372
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw <= 0) {
|
|
373
|
+
refuseArg(name, "max_results", `"max_results" must be a positive integer when supplied, got ${JSON.stringify(raw)}.`, batchIndex);
|
|
374
|
+
}
|
|
375
|
+
return raw as number;
|
|
376
|
+
}
|
|
377
|
+
|
|
325
378
|
/** Requires a present argument and hands it to `parseStoreAddress` -- the ONE
|
|
326
379
|
* address parser, which owns the `$`/`0x` forms and the deliberate refusal of
|
|
327
380
|
* an unprefixed numeric string. Absence is a DIFFERENT fact from malformity,
|
|
@@ -587,6 +640,53 @@ export const ANNO_TOOL_DEFINITIONS: readonly AnnoToolDefinition[] = [
|
|
|
587
640
|
required: ["store", "start_address", "end_address"],
|
|
588
641
|
},
|
|
589
642
|
},
|
|
643
|
+
{
|
|
644
|
+
name: "anno_exclude_range",
|
|
645
|
+
description:
|
|
646
|
+
"Records the user's request to leave an inclusive span out, WITH the reason, as a durable row (BUILD-05/BUILD-07). " +
|
|
647
|
+
"RECORDING AN EXCLUSION DOES NOT REMOVE ANYTHING: the export still emits every byte of that span; the record is " +
|
|
648
|
+
"what makes the request VISIBLE in the output instead of invisible as a gap. What gets reversed, kept or left out " +
|
|
649
|
+
"is the end-user's decision, and this verb is how the user states it -- it is not the tool deciding. An " +
|
|
650
|
+
"overlapping span is REFUSED naming both spans; two records that merely TOUCH at a boundary are disjoint and both " +
|
|
651
|
+
"accepted; an identical repeat SUCCEEDS reporting `changed: false`; the same extent with a DIFFERENT reason is " +
|
|
652
|
+
"REFUSED rather than overwriting the stored reason. MIND THE ENDS: one transposed end makes every later exclusion " +
|
|
653
|
+
"overlapping that start refuse -- use anno_include_range to undo it rather than burning revisions off the 32-deep " +
|
|
654
|
+
"snapshot ring.",
|
|
655
|
+
inputSchema: {
|
|
656
|
+
type: "object",
|
|
657
|
+
properties: {
|
|
658
|
+
...STORE_PROPERTY,
|
|
659
|
+
start_address: { description: "Start of the excluded span, INCLUSIVE. Integer, \"$hex\" or \"0x\" string." },
|
|
660
|
+
end_address: { description: "End of the excluded span, INCLUSIVE." },
|
|
661
|
+
reason: {
|
|
662
|
+
type: "string",
|
|
663
|
+
description:
|
|
664
|
+
"Why the user asked for this span to be left out. REQUIRED and must be non-empty: a reason column " +
|
|
665
|
+
"satisfied by an empty string records that something was excluded and loses WHY.",
|
|
666
|
+
},
|
|
667
|
+
...BASE_REVISION_PROPERTY,
|
|
668
|
+
},
|
|
669
|
+
required: ["store", "start_address", "end_address", "reason"],
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
name: "anno_include_range",
|
|
674
|
+
description:
|
|
675
|
+
"Removes the exclusion whose span is EXACTLY start_address..end_address -- the exact inverse of anno_exclude_range. " +
|
|
676
|
+
"Both stored ends must match exactly, because a record is never trimmed, split or partially removed. Read the " +
|
|
677
|
+
"stored spans with anno_exclude_range's sibling read (the excludedRanges list on either verb's own success body) " +
|
|
678
|
+
"first if you are unsure. Removing an exclusion that is not there SUCCEEDS and reports `changed: false`.",
|
|
679
|
+
inputSchema: {
|
|
680
|
+
type: "object",
|
|
681
|
+
properties: {
|
|
682
|
+
...STORE_PROPERTY,
|
|
683
|
+
start_address: { description: "Start of the exclusion to remove, INCLUSIVE. Must match the stored start exactly." },
|
|
684
|
+
end_address: { description: "End of the exclusion to remove, INCLUSIVE. Must match the stored end exactly." },
|
|
685
|
+
...BASE_REVISION_PROPERTY,
|
|
686
|
+
},
|
|
687
|
+
required: ["store", "start_address", "end_address"],
|
|
688
|
+
},
|
|
689
|
+
},
|
|
590
690
|
{
|
|
591
691
|
name: "anno_get_symbols",
|
|
592
692
|
description:
|
|
@@ -765,7 +865,11 @@ export const ANNO_TOOL_DEFINITIONS: readonly AnnoToolDefinition[] = [
|
|
|
765
865
|
"fail to reassemble. The extent is bounded by the SAME byte cap that governs anno_read_region -- one cap, both " +
|
|
766
866
|
"views, so there is no per-view rule to get subtly wrong -- and defaults to that cap when end_address is " +
|
|
767
867
|
"omitted. A wider range is REFUSED by name with the cap and the requested width in the message, never " +
|
|
768
|
-
"silently truncated."
|
|
868
|
+
"silently truncated. A register write bound to a project enum (via anno_apply_enum_usage) renders through " +
|
|
869
|
+
"its named member instead of a hex literal -- a single-field register as `#<enum>_<VARIANT>`, or, for a " +
|
|
870
|
+
"multi-field register, as its bits OR-ed together by name (`#D018_SELECT..0 | D018_CHARACTER..2 | " +
|
|
871
|
+
"D018_VIDEO..0`) with a trailing comment naming every field and its decoded value, so a bound write reads " +
|
|
872
|
+
"the same way here as it does in the exported ACME source.",
|
|
769
873
|
inputSchema: {
|
|
770
874
|
type: "object",
|
|
771
875
|
properties: {
|
|
@@ -936,6 +1040,283 @@ export const ANNO_TOOL_DEFINITIONS: readonly AnnoToolDefinition[] = [
|
|
|
936
1040
|
required: ["store", "calls"],
|
|
937
1041
|
},
|
|
938
1042
|
},
|
|
1043
|
+
{
|
|
1044
|
+
name: "anno_import_ghidra_export",
|
|
1045
|
+
description:
|
|
1046
|
+
"Imports a host-written Ghidra export transfer file (GhidraStructExport.java's `## `-delimited format) into " +
|
|
1047
|
+
"the store, writing one anno_xref row per surviving `## REFERENCES` line and DELETING the transfer file once " +
|
|
1048
|
+
"every write has durably committed. Costs one store open and one close. REFUSES, writes nothing and deletes " +
|
|
1049
|
+
"nothing: on a malformed, truncated or digest-mismatched export (naming the section and the offending line), " +
|
|
1050
|
+
"on an export_path that resolves outside the workspace root, or on an absent transfer file. Reports " +
|
|
1051
|
+
"referencesSeen, xrefsWritten, xrefsAlreadyPresent (duplicate references are deduped, never double-counted), " +
|
|
1052
|
+
"and kindsSeenNotImported -- reference types this store's four-member vocabulary does not carry, dropped and " +
|
|
1053
|
+
"counted rather than refused, because a real corpus binary carries ordinary jump and call references " +
|
|
1054
|
+
"constantly. Every written row's bank column is null: this verb does not resolve bank state itself. Also " +
|
|
1055
|
+
"reports constWrites -- the export's `## CONST_WRITES` facts (recovered $01/$D011/$D018/$DD00 stores), always " +
|
|
1056
|
+
"present (possibly empty). The transfer file naming them is DELETED by this same call (IMP-02), so this " +
|
|
1057
|
+
"return value is the only place they survive: pass the SAME constWrites array, unchanged, to a following " +
|
|
1058
|
+
"anno_join_memmap call's own const_writes argument to activate bank-state resolution (AUTO-04/AUTO-05) and " +
|
|
1059
|
+
"VIC-register graphics-range derivation (AUTO-06/AUTO-07) for this image.",
|
|
1060
|
+
inputSchema: {
|
|
1061
|
+
type: "object",
|
|
1062
|
+
properties: {
|
|
1063
|
+
...STORE_PROPERTY,
|
|
1064
|
+
export_path: {
|
|
1065
|
+
type: "string",
|
|
1066
|
+
description:
|
|
1067
|
+
"Absolute or workspace-relative path to the host-written transfer file. CONSUMED AND DELETED by a " +
|
|
1068
|
+
"successful call -- refused if it resolves outside the workspace root, including via a symlink.",
|
|
1069
|
+
},
|
|
1070
|
+
sha256: {
|
|
1071
|
+
type: "string",
|
|
1072
|
+
description:
|
|
1073
|
+
"Optional sha256 digest the producer reported for the transfer file's bytes. When supplied, a mismatch " +
|
|
1074
|
+
"against the file's own computed digest refuses the whole call before anything is read further -- a " +
|
|
1075
|
+
"corruption/drift detector, never a security boundary.",
|
|
1076
|
+
},
|
|
1077
|
+
...BASE_REVISION_PROPERTY,
|
|
1078
|
+
},
|
|
1079
|
+
required: ["store", "export_path"],
|
|
1080
|
+
},
|
|
1081
|
+
},
|
|
1082
|
+
{
|
|
1083
|
+
name: "anno_join_memmap",
|
|
1084
|
+
description:
|
|
1085
|
+
"The mechanical join: reads every distinct cross-reference target the store already holds, skips addresses " +
|
|
1086
|
+
"inside the supplied image's own loaded range (those are program addresses, never looked up), and annotates " +
|
|
1087
|
+
"every remaining address with the narrowest c64-memory-mapping/memmap.json entry containing it. No agent " +
|
|
1088
|
+
"call, no queue walk and no skill invocation anywhere in this call. Reports addressesConsidered, annotated, " +
|
|
1089
|
+
"skippedInImage, skippedNoMapEntry, declined and commentsChanged, plus a per-address decisions array naming " +
|
|
1090
|
+
"the outcome and, for every skip, WHY. Running this twice over an unchanged store reports commentsChanged: 0 " +
|
|
1091
|
+
"on the second run -- re-running a join pass is not an error. Passing const_writes (typically the SAME " +
|
|
1092
|
+
"constWrites array anno_import_ghidra_export just returned for this image, unchanged) additionally activates " +
|
|
1093
|
+
"bank-state resolution: a $01-conditional address (AUTO-04) declines with a named reason rather than " +
|
|
1094
|
+
"guessing when the reaching processor-port value is absent or disagreeing (AUTO-05), and VIC-register " +
|
|
1095
|
+
"graphics ranges are derived and written back (AUTO-06/AUTO-07, graphics_map_index selects which of several " +
|
|
1096
|
+
"derived combinations when more than one exists, default 0). Omitting const_writes entirely is a complete " +
|
|
1097
|
+
"no-op for both of these -- every address resolves exactly as if this argument did not exist.",
|
|
1098
|
+
inputSchema: {
|
|
1099
|
+
type: "object",
|
|
1100
|
+
properties: {
|
|
1101
|
+
...STORE_PROPERTY,
|
|
1102
|
+
...IMAGE_PROPERTY,
|
|
1103
|
+
...BASE_REVISION_PROPERTY,
|
|
1104
|
+
const_writes: {
|
|
1105
|
+
type: "array",
|
|
1106
|
+
items: {
|
|
1107
|
+
type: "object",
|
|
1108
|
+
properties: {
|
|
1109
|
+
store_address: { type: "integer", description: "The instruction's own address that performed the store." },
|
|
1110
|
+
target_address: { type: "integer", description: "The watched hardware address ($0001/$D011/$D018/$DD00) written to." },
|
|
1111
|
+
value: { type: "integer", description: "The compile-time constant value written." },
|
|
1112
|
+
},
|
|
1113
|
+
required: ["store_address", "target_address", "value"],
|
|
1114
|
+
},
|
|
1115
|
+
description:
|
|
1116
|
+
"Optional recovered const-write facts (AUTO-04/AUTO-05/AUTO-06/AUTO-07) -- pass back the constWrites " +
|
|
1117
|
+
"array anno_import_ghidra_export returned for the SAME image, unchanged. Supplying it (even []) " +
|
|
1118
|
+
"activates bank-state resolution and graphics-range derivation/write-back; omitting it entirely is a " +
|
|
1119
|
+
"complete no-op for both.",
|
|
1120
|
+
},
|
|
1121
|
+
graphics_map_index: {
|
|
1122
|
+
type: "integer",
|
|
1123
|
+
description:
|
|
1124
|
+
"Which of several derived VIC-register-value combinations to write back as graphics ranges, when " +
|
|
1125
|
+
"const_writes yields more than one distinct combination (D-37-27: several valid maps are never merged " +
|
|
1126
|
+
"into one). Defaults to 0. Consulted ONLY when const_writes is supplied at all. Out of range for the " +
|
|
1127
|
+
"derived map count REFUSES the whole call rather than silently clamping or picking a default.",
|
|
1128
|
+
},
|
|
1129
|
+
},
|
|
1130
|
+
required: ["store", "image"],
|
|
1131
|
+
},
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
name: "anno_evid_ingest",
|
|
1135
|
+
description:
|
|
1136
|
+
"Turns one raw memmapshow reply plus one run identity into durable runtime-execution evidence rows, so a later " +
|
|
1137
|
+
"session can query what the emulator actually executed instead of re-running the program. Writes a row ONLY for " +
|
|
1138
|
+
"an OBSERVED execute bit: an address memmapshow mentioned with read or write access but no execute gets NO row, " +
|
|
1139
|
+
"and an address the reply never mentioned at all gets NO row either -- an address with no row is the ABSENCE of " +
|
|
1140
|
+
"an assertion, never an assertion that the address is data. Requires the EXACT launch argv and digests it itself " +
|
|
1141
|
+
"(argv_digest is never accepted as an argument), so a caller cannot invent a run identity. A memmapshow reply " +
|
|
1142
|
+
"this surface cannot parse is REFUSED, naming its refusal code and offending line, rather than partially " +
|
|
1143
|
+
"absorbed -- nothing is written on a refusal. Re-ingesting the SAME reply for the SAME run identity succeeds " +
|
|
1144
|
+
"and reports changed:false with observationsWritten:0 -- re-running an ingest pass is not an error. Every count " +
|
|
1145
|
+
"in the answer carries a denominator (addressesQueried) beside it; no percentage is ever reported.",
|
|
1146
|
+
inputSchema: {
|
|
1147
|
+
type: "object",
|
|
1148
|
+
properties: {
|
|
1149
|
+
...STORE_PROPERTY,
|
|
1150
|
+
memmap_text: {
|
|
1151
|
+
type: "string",
|
|
1152
|
+
description:
|
|
1153
|
+
"The raw memmapshow reply exactly as the text monitor returned it -- never a pre-parsed object. A reply " +
|
|
1154
|
+
"this parser cannot decode is REFUSED, naming its refusal code and offending line; nothing is written.",
|
|
1155
|
+
},
|
|
1156
|
+
image_sha256: {
|
|
1157
|
+
type: "string",
|
|
1158
|
+
description:
|
|
1159
|
+
"The program image this run executed, named by the sha256 digest of its own bytes -- exactly 64 " +
|
|
1160
|
+
"lowercase hex characters. This verb does not read image bytes itself and accepts no path to one.",
|
|
1161
|
+
},
|
|
1162
|
+
argv: {
|
|
1163
|
+
type: "array",
|
|
1164
|
+
items: { type: "string" },
|
|
1165
|
+
description:
|
|
1166
|
+
"The EXACT emulator launch argument vector, including argv[0] -- a different binary is a different " +
|
|
1167
|
+
"launch. This verb digests it itself; a pre-computed digest is never accepted, so a caller cannot invent " +
|
|
1168
|
+
"a run identity.",
|
|
1169
|
+
},
|
|
1170
|
+
seed: {
|
|
1171
|
+
type: "string",
|
|
1172
|
+
description: "The determinism seed the launch pinned. A non-empty string; not a digest and carries no shape beyond that.",
|
|
1173
|
+
},
|
|
1174
|
+
...BASE_REVISION_PROPERTY,
|
|
1175
|
+
},
|
|
1176
|
+
required: ["store", "memmap_text", "image_sha256", "argv", "seed"],
|
|
1177
|
+
},
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
name: "anno_evid_disagreements",
|
|
1181
|
+
description:
|
|
1182
|
+
"Answers where the byte-derived block classification and the observed-execution evidence DISAGREE, with the " +
|
|
1183
|
+
"disagreements reported FIRST: an address the block table calls 'data' at which the emulator was observed " +
|
|
1184
|
+
"executing is proof a byte-derived guess was wrong, from a source (real execution) that never saw the guess. " +
|
|
1185
|
+
"Agreement (block table says 'code', evidence confirms it) is reported as agreementCount ONLY -- never as rows, " +
|
|
1186
|
+
"because a wall of agreeing rows would bury the one output this query exists to surface. An address the block " +
|
|
1187
|
+
"table covers with NO observation anywhere is blockCoveredNeverObservedCount, and is NOT evidence that the " +
|
|
1188
|
+
"address is data -- an address never observed executing proves nothing. Two further counts " +
|
|
1189
|
+
"(observedOutsideAnyBlockCount, observedAtUndefinedBlockCount) name evidence about addresses the block table " +
|
|
1190
|
+
"does not classify as code or data at all, so the denominator can never quietly drop real evidence. This verb " +
|
|
1191
|
+
"READS the block table and the runtime evidence table; it writes to NEITHER, and a repeated call never changes " +
|
|
1192
|
+
"either. Optional image_sha256/argv_digest/seed scope the question to ONE run identity's observations rather " +
|
|
1193
|
+
"than the union across every run that has ever contributed -- supply all three together or none; a partial " +
|
|
1194
|
+
"identity is refused. max_results bounds the returned disagreements array only, and is OPTIONAL (an empty or " +
|
|
1195
|
+
"small disagreement report is the ordinary, sound case, so no ceiling is forced); the true disagreement count " +
|
|
1196
|
+
"and whether truncation occurred are always reported beside it. Every count in the answer carries denominator " +
|
|
1197
|
+
"beside it; no percentage or rate is ever formed.",
|
|
1198
|
+
inputSchema: {
|
|
1199
|
+
type: "object",
|
|
1200
|
+
properties: {
|
|
1201
|
+
...STORE_PROPERTY,
|
|
1202
|
+
max_results: {
|
|
1203
|
+
type: "integer",
|
|
1204
|
+
description:
|
|
1205
|
+
"Optional bound on the returned disagreements array only. Unlike every other list-returning anno_* verb, " +
|
|
1206
|
+
"this is NOT required -- an empty or small disagreement report is the ordinary, sound case. When " +
|
|
1207
|
+
"supplied, must be a positive integer.",
|
|
1208
|
+
},
|
|
1209
|
+
image_sha256: {
|
|
1210
|
+
type: "string",
|
|
1211
|
+
description:
|
|
1212
|
+
"Optional run-identity filter: the program image this run executed, exactly 64 lowercase hex characters. " +
|
|
1213
|
+
"Supply image_sha256, argv_digest AND seed together to scope to one run, or omit all three to see the " +
|
|
1214
|
+
"union across every run this store holds.",
|
|
1215
|
+
},
|
|
1216
|
+
argv_digest: {
|
|
1217
|
+
type: "string",
|
|
1218
|
+
description:
|
|
1219
|
+
"Optional run-identity filter: the exact digest anno_evid_ingest/anno_evid_runs already computed for a " +
|
|
1220
|
+
"run's launch argv, exactly 64 lowercase hex characters. Never invented by a caller -- pass back what " +
|
|
1221
|
+
"anno_evid_runs reported. Required alongside image_sha256/seed when filtering by run identity.",
|
|
1222
|
+
},
|
|
1223
|
+
seed: {
|
|
1224
|
+
type: "string",
|
|
1225
|
+
description:
|
|
1226
|
+
"Optional run-identity filter: the determinism seed that run's launch pinned. A non-empty string. " +
|
|
1227
|
+
"Required alongside image_sha256/argv_digest when filtering by run identity.",
|
|
1228
|
+
},
|
|
1229
|
+
},
|
|
1230
|
+
required: ["store"],
|
|
1231
|
+
},
|
|
1232
|
+
},
|
|
1233
|
+
{
|
|
1234
|
+
name: "anno_evid_runs",
|
|
1235
|
+
description:
|
|
1236
|
+
"Answers every run identity the store holds an observed-execution row for, with its accumulated observation " +
|
|
1237
|
+
"count and the denominator that count is a fraction of -- so a later session can see what evidence already " +
|
|
1238
|
+
"exists without re-running the program. However many runs contribute observations, their union is NEVER " +
|
|
1239
|
+
"exhaustive coverage of the image: observationCount is a count against denominator, never a rate, and this " +
|
|
1240
|
+
"verb forms no percentage from it.",
|
|
1241
|
+
inputSchema: {
|
|
1242
|
+
type: "object",
|
|
1243
|
+
properties: {
|
|
1244
|
+
...STORE_PROPERTY,
|
|
1245
|
+
},
|
|
1246
|
+
required: ["store"],
|
|
1247
|
+
},
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
name: "anno_evid_reset",
|
|
1251
|
+
description:
|
|
1252
|
+
"Clears every observed-execution row for ONE run identity, so that bracket can be re-measured from nothing. " +
|
|
1253
|
+
"Touches no other run identity's rows and no row of the byte-derived block table. Requires the EXACT launch " +
|
|
1254
|
+
"argv and digests it itself (a pre-computed digest is never accepted), so a caller cannot invent a run identity " +
|
|
1255
|
+
"-- the same discipline anno_evid_ingest uses. A run identity holding no observations SUCCEEDS and reports " +
|
|
1256
|
+
"changed:false and observationsRemoved:0 -- resetting an empty bracket is the ordinary thing, not a mistake. " +
|
|
1257
|
+
"Clearing the emulator's own accumulated access map is a DIFFERENT operation, reached through vice_memmap_zap " +
|
|
1258
|
+
"-- a caller re-measuring a bracket from nothing does BOTH: vice_memmap_zap on the emulator side, " +
|
|
1259
|
+
"anno_evid_reset on the store side.",
|
|
1260
|
+
inputSchema: {
|
|
1261
|
+
type: "object",
|
|
1262
|
+
properties: {
|
|
1263
|
+
...STORE_PROPERTY,
|
|
1264
|
+
image_sha256: {
|
|
1265
|
+
type: "string",
|
|
1266
|
+
description:
|
|
1267
|
+
"The program image this run executed, named by the sha256 digest of its own bytes -- exactly 64 " +
|
|
1268
|
+
"lowercase hex characters. This verb does not read image bytes itself and accepts no path to one.",
|
|
1269
|
+
},
|
|
1270
|
+
argv: {
|
|
1271
|
+
type: "array",
|
|
1272
|
+
items: { type: "string" },
|
|
1273
|
+
description:
|
|
1274
|
+
"The EXACT emulator launch argument vector, including argv[0] -- a different binary is a different " +
|
|
1275
|
+
"launch. This verb digests it itself; a pre-computed digest is never accepted, so a caller cannot invent " +
|
|
1276
|
+
"a run identity.",
|
|
1277
|
+
},
|
|
1278
|
+
seed: {
|
|
1279
|
+
type: "string",
|
|
1280
|
+
description: "The determinism seed the launch pinned. A non-empty string; not a digest and carries no shape beyond that.",
|
|
1281
|
+
},
|
|
1282
|
+
...BASE_REVISION_PROPERTY,
|
|
1283
|
+
},
|
|
1284
|
+
required: ["store", "image_sha256", "argv", "seed"],
|
|
1285
|
+
},
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
name: "anno_hazard_report",
|
|
1289
|
+
description:
|
|
1290
|
+
"Enumerates what blocks a program's code or data from being MOVED, relocated, rebased or stripped, across " +
|
|
1291
|
+
"the movement-hazard constructions this surface can detect from decoded bytes alone. It REPORTS " +
|
|
1292
|
+
"and changes NOTHING: it never removes, strips, relocates or rebases any part of the image, and it never " +
|
|
1293
|
+
"emits an instruction, flag or field a caller could act on as an automatic relocation -- the operator " +
|
|
1294
|
+
"decides what happens to the bytes it describes. Each finding carries its own detection mechanism and a " +
|
|
1295
|
+
"detection-strength token (observed-corroborated, static-shape-matched, static-signature-only) -- a " +
|
|
1296
|
+
"SEPARATE, smaller vocabulary from this store's own five-grade confidence grades, answering a different " +
|
|
1297
|
+
"question (how strong is this ONE static signal, never what does this address classify as). Every checked " +
|
|
1298
|
+
"region reports exactly one of three outcomes -- hazard-reported, no-signal, unclassified -- and NONE of " +
|
|
1299
|
+
"them is a safety claim: a region with no finding is explicitly NOT a claim that the region is safe to " +
|
|
1300
|
+
"move, clean, or hazard-free, only that nothing this report knows how to look for fired there. " +
|
|
1301
|
+
"Always-emitted named limits (for example, a self-modification through a runtime-computed pointer is " +
|
|
1302
|
+
"undetected by construction) accompany every answer. Opens the store READ-ONLY and reads no other table: " +
|
|
1303
|
+
"this verb creates nothing and writes nothing.",
|
|
1304
|
+
inputSchema: {
|
|
1305
|
+
type: "object",
|
|
1306
|
+
properties: {
|
|
1307
|
+
...STORE_PROPERTY,
|
|
1308
|
+
...IMAGE_PROPERTY,
|
|
1309
|
+
max_results: {
|
|
1310
|
+
type: "integer",
|
|
1311
|
+
description:
|
|
1312
|
+
"Optional bound on the returned findings array only. Unlike most list-returning anno_* verbs, this is " +
|
|
1313
|
+
"NOT required -- an empty or small finding set is the ordinary, sound case. When supplied, must be a " +
|
|
1314
|
+
"positive integer.",
|
|
1315
|
+
},
|
|
1316
|
+
},
|
|
1317
|
+
required: ["store", "image"],
|
|
1318
|
+
},
|
|
1319
|
+
},
|
|
939
1320
|
];
|
|
940
1321
|
|
|
941
1322
|
/** The allow-list, DERIVED from the definitions above rather than hand-typed
|
|
@@ -1005,6 +1386,33 @@ function assertScopeArgs(name: string, args: unknown, batchIndex?: number): void
|
|
|
1005
1386
|
assertBaseRevisionArg(name, args, batchIndex);
|
|
1006
1387
|
}
|
|
1007
1388
|
|
|
1389
|
+
/** Shared validator for `anno_exclude_range` / `anno_include_range`, called
|
|
1390
|
+
* from `assertVerbArgs()` by two arms so the direct route and
|
|
1391
|
+
* `anno_batch_execute`'s inner loop cannot diverge (mirrors `assertScopeArgs`
|
|
1392
|
+
* exactly). "byte" selects the same two span shape rules a scope uses --
|
|
1393
|
+
* an exclusion is not a table. `reason` is required ONLY for the setter: the
|
|
1394
|
+
* unsetter names an existing record by its span alone. This layer refuses an
|
|
1395
|
+
* absent, non-string or empty/whitespace-only reason at the surface; the
|
|
1396
|
+
* store's own `assertCommentText()` re-checks the full comment-text
|
|
1397
|
+
* vocabulary at write time (T-46-01) -- this is not a second, divergent rule,
|
|
1398
|
+
* only an earlier gate on the same three malformed shapes. */
|
|
1399
|
+
function assertExcludedRangeArgs(name: string, args: unknown, batchIndex?: number): void {
|
|
1400
|
+
assertStoreArg(name, args, batchIndex);
|
|
1401
|
+
assertSpanArgs(name, args, "byte", batchIndex);
|
|
1402
|
+
if (name === "anno_exclude_range") {
|
|
1403
|
+
const reason = argBag(args).reason;
|
|
1404
|
+
if (typeof reason !== "string" || reason.trim() === "") {
|
|
1405
|
+
refuseArg(
|
|
1406
|
+
name,
|
|
1407
|
+
"reason",
|
|
1408
|
+
`"reason" must be a non-empty string stating why the user asked for this span to be left out, got ${JSON.stringify(reason)}.`,
|
|
1409
|
+
batchIndex,
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
assertBaseRevisionArg(name, args, batchIndex);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1008
1416
|
function assertGetCommentsArgs(args: unknown, batchIndex?: number): void {
|
|
1009
1417
|
assertStoreArg("anno_get_comments", args, batchIndex);
|
|
1010
1418
|
assertMaxResults("anno_get_comments", args, batchIndex);
|
|
@@ -1097,6 +1505,222 @@ function assertSaveProjectArgs(args: unknown, batchIndex?: number): void {
|
|
|
1097
1505
|
assertStoreArg("anno_save_project", args, batchIndex);
|
|
1098
1506
|
}
|
|
1099
1507
|
|
|
1508
|
+
function assertImportGhidraExportArgs(args: unknown, batchIndex?: number): void {
|
|
1509
|
+
assertStoreArg("anno_import_ghidra_export", args, batchIndex);
|
|
1510
|
+
const bag = argBag(args);
|
|
1511
|
+
if (typeof bag.export_path !== "string" || bag.export_path.trim() === "") {
|
|
1512
|
+
refuseArg("anno_import_ghidra_export", "export_path", '"export_path" is required and must be a non-empty string.', batchIndex);
|
|
1513
|
+
}
|
|
1514
|
+
if (bag.sha256 !== undefined && (typeof bag.sha256 !== "string" || bag.sha256.trim() === "")) {
|
|
1515
|
+
refuseArg("anno_import_ghidra_export", "sha256", '"sha256" must be a non-empty string when supplied.', batchIndex);
|
|
1516
|
+
}
|
|
1517
|
+
assertBaseRevisionArg("anno_import_ghidra_export", args, batchIndex);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/** Validates one `const_writes[i]` element against the wire shape declared on
|
|
1521
|
+
* `anno_join_memmap`'s own schema, and narrows it to a `ConstWriteFact`
|
|
1522
|
+
* (CR-01 fix). Each of the three fields is required and must be a
|
|
1523
|
+
* non-negative integer -- these are ALREADY-RESOLVED facts a caller is
|
|
1524
|
+
* round-tripping from a prior anno_import_ghidra_export call, never an
|
|
1525
|
+
* agent-typed address, so there is no `$`/`0x` ambiguity to route through
|
|
1526
|
+
* `parseStoreAddress()` here. */
|
|
1527
|
+
function assertConstWriteFactArg(name: string, raw: unknown, index: number, batchIndex?: number): ConstWriteFact {
|
|
1528
|
+
if (!isPlainObject(raw)) {
|
|
1529
|
+
refuseArg(
|
|
1530
|
+
name,
|
|
1531
|
+
"const_writes",
|
|
1532
|
+
`"const_writes[${index}]" must be an object with store_address/target_address/value fields, got ${JSON.stringify(raw)}.`,
|
|
1533
|
+
batchIndex,
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
const bag = raw as Record<string, unknown>;
|
|
1537
|
+
for (const key of ["store_address", "target_address", "value"] as const) {
|
|
1538
|
+
const value = bag[key];
|
|
1539
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
1540
|
+
refuseArg(
|
|
1541
|
+
name,
|
|
1542
|
+
"const_writes",
|
|
1543
|
+
`"const_writes[${index}].${key}" must be a non-negative integer, got ${JSON.stringify(value)}.`,
|
|
1544
|
+
batchIndex,
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
return {
|
|
1549
|
+
storeAddress: bag.store_address as number,
|
|
1550
|
+
targetAddress: bag.target_address as number,
|
|
1551
|
+
value: bag.value as number,
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/** Validates the optional `const_writes` array, returning `undefined` when
|
|
1556
|
+
* omitted -- OMISSION, not emptiness, is what `runMemmapJoin()` treats as
|
|
1557
|
+
* "skip the bank-state/graphics machinery entirely" (D-37-24's own
|
|
1558
|
+
* documented activation switch), so this must not default an absent
|
|
1559
|
+
* argument to `[]`. */
|
|
1560
|
+
function assertConstWritesArg(name: string, args: unknown, batchIndex?: number): ConstWriteFact[] | undefined {
|
|
1561
|
+
const raw = argBag(args).const_writes;
|
|
1562
|
+
if (raw === undefined) return undefined;
|
|
1563
|
+
if (!Array.isArray(raw)) {
|
|
1564
|
+
refuseArg(name, "const_writes", `"const_writes" must be an array when supplied, got ${JSON.stringify(raw)}.`, batchIndex);
|
|
1565
|
+
}
|
|
1566
|
+
return raw.map((entry, i) => assertConstWriteFactArg(name, entry, i, batchIndex));
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/** Validates the optional `graphics_map_index` argument. */
|
|
1570
|
+
function assertGraphicsMapIndexArg(name: string, args: unknown, batchIndex?: number): number | undefined {
|
|
1571
|
+
const raw = argBag(args).graphics_map_index;
|
|
1572
|
+
if (raw === undefined) return undefined;
|
|
1573
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
|
|
1574
|
+
refuseArg(
|
|
1575
|
+
name,
|
|
1576
|
+
"graphics_map_index",
|
|
1577
|
+
`"graphics_map_index" must be a non-negative integer when supplied, got ${JSON.stringify(raw)}.`,
|
|
1578
|
+
batchIndex,
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
return raw;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
function assertJoinMemmapArgs(args: unknown, batchIndex?: number): void {
|
|
1585
|
+
assertStoreArg("anno_join_memmap", args, batchIndex);
|
|
1586
|
+
assertImageArg("anno_join_memmap", args, batchIndex);
|
|
1587
|
+
assertBaseRevisionArg("anno_join_memmap", args, batchIndex);
|
|
1588
|
+
assertConstWritesArg("anno_join_memmap", args, batchIndex);
|
|
1589
|
+
assertGraphicsMapIndexArg("anno_join_memmap", args, batchIndex);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/** The run-identity digest shape: exactly 64 lowercase hex characters. This
|
|
1593
|
+
* module's own copy of the check (mirroring `evid-ingest.ts`'s identical,
|
|
1594
|
+
* deliberately un-imported copy): `image_sha256` never reaches a digest
|
|
1595
|
+
* function here, so there is nothing to route through a shared regex, and a
|
|
1596
|
+
* caller-visible refusal must fire BEFORE any store is opened -- before
|
|
1597
|
+
* `evid-ingest.ts`'s own `runIdentityFrom()` ever runs. */
|
|
1598
|
+
const EVID_DIGEST_RE = /^[0-9a-f]{64}$/;
|
|
1599
|
+
|
|
1600
|
+
/** `anno_evid_ingest`'s own argument assertion, wired into `assertVerbArgs`
|
|
1601
|
+
* beside `anno_join_memmap`'s. Refuses BY NAME, before any store is opened: a
|
|
1602
|
+
* non-string/empty `memmap_text`, an `image_sha256` that is not exactly 64
|
|
1603
|
+
* lowercase hex characters, an `argv` that is not a non-empty array of
|
|
1604
|
+
* strings, and a `seed` that is not a non-empty string (T-43-21). */
|
|
1605
|
+
function assertEvidIngestArgs(args: unknown, batchIndex?: number): void {
|
|
1606
|
+
assertStoreArg("anno_evid_ingest", args, batchIndex);
|
|
1607
|
+
assertBaseRevisionArg("anno_evid_ingest", args, batchIndex);
|
|
1608
|
+
const bag = argBag(args);
|
|
1609
|
+
if (typeof bag.memmap_text !== "string" || bag.memmap_text.trim() === "") {
|
|
1610
|
+
refuseArg(
|
|
1611
|
+
"anno_evid_ingest",
|
|
1612
|
+
"memmap_text",
|
|
1613
|
+
`"memmap_text" must be a non-empty string carrying the raw memmapshow reply, got ${JSON.stringify(bag.memmap_text)}.`,
|
|
1614
|
+
batchIndex,
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
if (typeof bag.image_sha256 !== "string" || !EVID_DIGEST_RE.test(bag.image_sha256)) {
|
|
1618
|
+
refuseArg(
|
|
1619
|
+
"anno_evid_ingest",
|
|
1620
|
+
"image_sha256",
|
|
1621
|
+
`"image_sha256" must be exactly 64 lowercase hex characters, got ${JSON.stringify(bag.image_sha256)}.`,
|
|
1622
|
+
batchIndex,
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
if (!Array.isArray(bag.argv) || bag.argv.length === 0 || bag.argv.some((entry) => typeof entry !== "string")) {
|
|
1626
|
+
refuseArg(
|
|
1627
|
+
"anno_evid_ingest",
|
|
1628
|
+
"argv",
|
|
1629
|
+
`"argv" must be a non-empty array of strings naming the exact emulator launch argument vector, got ${JSON.stringify(bag.argv)}.`,
|
|
1630
|
+
batchIndex,
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
if (typeof bag.seed !== "string" || bag.seed.length === 0) {
|
|
1634
|
+
refuseArg("anno_evid_ingest", "seed", `"seed" must be a non-empty string, got ${JSON.stringify(bag.seed)}.`, batchIndex);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/** `anno_evid_disagreements`'s own argument assertion (plan 43-06). The
|
|
1639
|
+
* three run-identity filters are ALL-OR-NONE, mirroring
|
|
1640
|
+
* `listExecObservations()`'s own rule in `anno-store.ts` exactly: a partial
|
|
1641
|
+
* identity would silently widen the match to every run sharing the supplied
|
|
1642
|
+
* field, which is not what "filter by run identity" means. */
|
|
1643
|
+
function assertEvidDisagreementsArgs(args: unknown, batchIndex?: number): void {
|
|
1644
|
+
assertStoreArg("anno_evid_disagreements", args, batchIndex);
|
|
1645
|
+
assertOptionalMaxResults("anno_evid_disagreements", args, batchIndex);
|
|
1646
|
+
const bag = argBag(args);
|
|
1647
|
+
const filterFieldsGiven = [bag.image_sha256, bag.argv_digest, bag.seed].filter((v) => v !== undefined).length;
|
|
1648
|
+
if (filterFieldsGiven > 0 && filterFieldsGiven < 3) {
|
|
1649
|
+
refuseArg(
|
|
1650
|
+
"anno_evid_disagreements",
|
|
1651
|
+
"image_sha256",
|
|
1652
|
+
"a run-identity filter requires image_sha256, argv_digest AND seed together -- a partial identity would " +
|
|
1653
|
+
"silently widen the match to every run sharing the supplied field(s).",
|
|
1654
|
+
batchIndex,
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
if (filterFieldsGiven === 3) {
|
|
1658
|
+
if (typeof bag.image_sha256 !== "string" || !EVID_DIGEST_RE.test(bag.image_sha256)) {
|
|
1659
|
+
refuseArg(
|
|
1660
|
+
"anno_evid_disagreements",
|
|
1661
|
+
"image_sha256",
|
|
1662
|
+
`"image_sha256" must be exactly 64 lowercase hex characters, got ${JSON.stringify(bag.image_sha256)}.`,
|
|
1663
|
+
batchIndex,
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
if (typeof bag.argv_digest !== "string" || !EVID_DIGEST_RE.test(bag.argv_digest)) {
|
|
1667
|
+
refuseArg(
|
|
1668
|
+
"anno_evid_disagreements",
|
|
1669
|
+
"argv_digest",
|
|
1670
|
+
`"argv_digest" must be exactly 64 lowercase hex characters, got ${JSON.stringify(bag.argv_digest)}.`,
|
|
1671
|
+
batchIndex,
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
if (typeof bag.seed !== "string" || bag.seed.length === 0) {
|
|
1675
|
+
refuseArg("anno_evid_disagreements", "seed", `"seed" must be a non-empty string, got ${JSON.stringify(bag.seed)}.`, batchIndex);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
/** `anno_evid_runs`'s own argument assertion (plan 43-06): just the
|
|
1681
|
+
* universal `store` argument, since this verb takes no other input. */
|
|
1682
|
+
function assertEvidRunsArgs(args: unknown, batchIndex?: number): void {
|
|
1683
|
+
assertStoreArg("anno_evid_runs", args, batchIndex);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
/** `anno_evid_reset`'s own argument assertion (plan 43-06), the SAME shape
|
|
1687
|
+
* as `assertEvidIngestArgs` minus `memmap_text` -- refuses BY NAME, before
|
|
1688
|
+
* any store is opened: an `image_sha256` that is not exactly 64 lowercase
|
|
1689
|
+
* hex characters, an `argv` that is not a non-empty array of strings, and a
|
|
1690
|
+
* `seed` that is not a non-empty string. */
|
|
1691
|
+
function assertEvidResetArgs(args: unknown, batchIndex?: number): void {
|
|
1692
|
+
assertStoreArg("anno_evid_reset", args, batchIndex);
|
|
1693
|
+
assertBaseRevisionArg("anno_evid_reset", args, batchIndex);
|
|
1694
|
+
const bag = argBag(args);
|
|
1695
|
+
if (typeof bag.image_sha256 !== "string" || !EVID_DIGEST_RE.test(bag.image_sha256)) {
|
|
1696
|
+
refuseArg(
|
|
1697
|
+
"anno_evid_reset",
|
|
1698
|
+
"image_sha256",
|
|
1699
|
+
`"image_sha256" must be exactly 64 lowercase hex characters, got ${JSON.stringify(bag.image_sha256)}.`,
|
|
1700
|
+
batchIndex,
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
if (!Array.isArray(bag.argv) || bag.argv.length === 0 || bag.argv.some((entry) => typeof entry !== "string")) {
|
|
1704
|
+
refuseArg(
|
|
1705
|
+
"anno_evid_reset",
|
|
1706
|
+
"argv",
|
|
1707
|
+
`"argv" must be a non-empty array of strings naming the exact emulator launch argument vector, got ${JSON.stringify(bag.argv)}.`,
|
|
1708
|
+
batchIndex,
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
if (typeof bag.seed !== "string" || bag.seed.length === 0) {
|
|
1712
|
+
refuseArg("anno_evid_reset", "seed", `"seed" must be a non-empty string, got ${JSON.stringify(bag.seed)}.`, batchIndex);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/** `anno_hazard_report`'s own argument assertion. Reuses the shared store,
|
|
1717
|
+
* image and optional-max-results assertions rather than inlining a fourth
|
|
1718
|
+
* check -- this verb has no argument shape of its own beyond those three. */
|
|
1719
|
+
function assertHazardReportArgs(args: unknown, batchIndex?: number): void {
|
|
1720
|
+
assertStoreArg("anno_hazard_report", args, batchIndex);
|
|
1721
|
+
assertImageArg("anno_hazard_report", args, batchIndex);
|
|
1722
|
+
assertOptionalMaxResults("anno_hazard_report", args, batchIndex);
|
|
1723
|
+
}
|
|
1100
1724
|
|
|
1101
1725
|
// ---------------------------------------------------------------------------
|
|
1102
1726
|
// THE ONE SIZE CAP, GOVERNING BOTH VIEWS (T-29-25).
|
|
@@ -1109,12 +1733,17 @@ function assertSaveProjectArgs(args: unknown, batchIndex?: number): void {
|
|
|
1109
1733
|
// the disassembly view at the cap is the worst case, since the hexdump view of
|
|
1110
1734
|
// the same byte count renders far less text.
|
|
1111
1735
|
//
|
|
1112
|
-
//
|
|
1113
|
-
// `wrapPossiblyChunked()`
|
|
1114
|
-
//
|
|
1115
|
-
//
|
|
1116
|
-
//
|
|
1117
|
-
//
|
|
1736
|
+
// THIS CAP IS NOT THE ONLY BOUND FOR THIS FAMILY, BUT IT REMAINS THE
|
|
1737
|
+
// LOAD-BEARING ONE. `vice-proxy.ts`'s `wrapPossiblyChunked()` runs at the
|
|
1738
|
+
// proxy's single tools/call choke point -- the one place every registered
|
|
1739
|
+
// tool's result is checked before it reaches the wire -- so an over-cap
|
|
1740
|
+
// answer from this family crosses that same override exactly like any
|
|
1741
|
+
// other tool's, and is split across a continuation sequence rather than
|
|
1742
|
+
// delivered whole. That does not make this cap redundant: the client's own
|
|
1743
|
+
// inline-response ceiling was measured at 40-60 KB, far below the proxy's
|
|
1744
|
+
// 500,000-character output cap, so a result that never trips the proxy's
|
|
1745
|
+
// split can still be far too large to be useful. That is why the second
|
|
1746
|
+
// mitigation -- `max_results` REQUIRED with no default on every
|
|
1118
1747
|
// list-returning verb, with the true total returned beside the truncated list
|
|
1119
1748
|
// -- is not optional either.
|
|
1120
1749
|
// ---------------------------------------------------------------------------
|
|
@@ -1399,12 +2028,21 @@ function assertVerbArgs(name: string, args: unknown, batchIndex?: number): void
|
|
|
1399
2028
|
if (name === "anno_set_data_type") return assertSetDataTypeArgs(args, batchIndex);
|
|
1400
2029
|
if (name === "anno_add_scope") return assertScopeArgs("anno_add_scope", args, batchIndex);
|
|
1401
2030
|
if (name === "anno_remove_scope") return assertScopeArgs("anno_remove_scope", args, batchIndex);
|
|
2031
|
+
if (name === "anno_exclude_range") return assertExcludedRangeArgs("anno_exclude_range", args, batchIndex);
|
|
2032
|
+
if (name === "anno_include_range") return assertExcludedRangeArgs("anno_include_range", args, batchIndex);
|
|
1402
2033
|
if (name === "anno_get_comments") return assertGetCommentsArgs(args, batchIndex);
|
|
1403
2034
|
if (name === "anno_get_blocks") return assertGetBlocksArgs(args, batchIndex);
|
|
1404
2035
|
if (name === "anno_create_project_enum") return assertCreateEnumArgs(args, batchIndex);
|
|
1405
2036
|
if (name === "anno_update_project_enum") return assertUpdateEnumArgs(args, batchIndex);
|
|
1406
2037
|
if (name === "anno_apply_enum_usage") return assertApplyEnumUsageArgs(args, batchIndex);
|
|
1407
2038
|
if (name === "anno_save_project") return assertSaveProjectArgs(args, batchIndex);
|
|
2039
|
+
if (name === "anno_import_ghidra_export") return assertImportGhidraExportArgs(args, batchIndex);
|
|
2040
|
+
if (name === "anno_join_memmap") return assertJoinMemmapArgs(args, batchIndex);
|
|
2041
|
+
if (name === "anno_evid_ingest") return assertEvidIngestArgs(args, batchIndex);
|
|
2042
|
+
if (name === "anno_evid_disagreements") return assertEvidDisagreementsArgs(args, batchIndex);
|
|
2043
|
+
if (name === "anno_evid_runs") return assertEvidRunsArgs(args, batchIndex);
|
|
2044
|
+
if (name === "anno_evid_reset") return assertEvidResetArgs(args, batchIndex);
|
|
2045
|
+
if (name === "anno_hazard_report") return assertHazardReportArgs(args, batchIndex);
|
|
1408
2046
|
if (name === "anno_disassemble") return assertDisassembleArgs(args, batchIndex);
|
|
1409
2047
|
if (name === "anno_read_region") return assertReadRegionArgs(args, batchIndex);
|
|
1410
2048
|
if (name === "anno_get_binary_info") return assertBinaryInfoArgs(args, batchIndex);
|
|
@@ -1424,7 +2062,7 @@ function assertVerbArgs(name: string, args: unknown, batchIndex?: number): void
|
|
|
1424
2062
|
|
|
1425
2063
|
/**
|
|
1426
2064
|
* The allow-list gate. Its body's FIRST check is set membership (see WHAT NOT
|
|
1427
|
-
* TO DO above, and
|
|
2065
|
+
* TO DO above, and the same confused-deputy precedent inverted into an
|
|
1428
2066
|
* allow-list): a `name` outside `CURATED_ANNO_TOOLS` is refused outright,
|
|
1429
2067
|
* before any argument is inspected, so an unknown verb can never reach a
|
|
1430
2068
|
* validator that might coincidentally accept its payload. Only then are the
|
|
@@ -1493,7 +2131,7 @@ function resolveStoreArg(name: string, args: unknown): string {
|
|
|
1493
2131
|
* takes the existence-check-plus-inode-guard route below. Derived from nothing
|
|
1494
2132
|
* -- it is a hand-listed property of each verb, and a verb missing from here is
|
|
1495
2133
|
* merely opened writably, never wrongly refused. */
|
|
1496
|
-
const READ_ONLY_ANNO_VERBS: readonly string[] = Object.freeze([
|
|
2134
|
+
export const READ_ONLY_ANNO_VERBS: readonly string[] = Object.freeze([
|
|
1497
2135
|
"anno_get_symbols",
|
|
1498
2136
|
"anno_get_comments",
|
|
1499
2137
|
"anno_get_blocks",
|
|
@@ -1504,6 +2142,9 @@ const READ_ONLY_ANNO_VERBS: readonly string[] = Object.freeze([
|
|
|
1504
2142
|
"anno_get_cross_references",
|
|
1505
2143
|
"anno_search",
|
|
1506
2144
|
"anno_get_address_details",
|
|
2145
|
+
"anno_evid_disagreements",
|
|
2146
|
+
"anno_evid_runs",
|
|
2147
|
+
"anno_hazard_report",
|
|
1507
2148
|
]);
|
|
1508
2149
|
|
|
1509
2150
|
/** Refuses an absent store BY NAME, returning the inode the later guard
|
|
@@ -1627,6 +2268,31 @@ function dispatchScope(name: string, handle: AnnoStoreHandle, args: unknown): un
|
|
|
1627
2268
|
};
|
|
1628
2269
|
}
|
|
1629
2270
|
|
|
2271
|
+
/** One dispatcher serving `anno_exclude_range` / `anno_include_range`,
|
|
2272
|
+
* modelled on `dispatchScope()`. `excludedRanges` rides on EVERY successful
|
|
2273
|
+
* body, including when it is empty, for the same reason `dispatchSetDataType`'s
|
|
2274
|
+
* own disclosures do: the resulting state is a fact the caller is told, not
|
|
2275
|
+
* the absence of a field it has to know to look for. */
|
|
2276
|
+
function dispatchExcludedRange(name: string, handle: AnnoStoreHandle, args: unknown): unknown {
|
|
2277
|
+
const bag = argBag(args);
|
|
2278
|
+
const span = {
|
|
2279
|
+
start: bag.start_address as number | string,
|
|
2280
|
+
endInclusive: bag.end_address as number | string,
|
|
2281
|
+
baseRevision: assertBaseRevisionArg(name, args),
|
|
2282
|
+
};
|
|
2283
|
+
const written =
|
|
2284
|
+
name === "anno_exclude_range"
|
|
2285
|
+
? addExcludedRange(handle, { ...span, reason: bag.reason as string })
|
|
2286
|
+
: removeExcludedRange(handle, span);
|
|
2287
|
+
return {
|
|
2288
|
+
store: handle.path,
|
|
2289
|
+
start_address: parseStoreAddress(bag.start_address, { what: "start_address" }),
|
|
2290
|
+
end_address: parseStoreAddress(bag.end_address, { what: "end_address" }),
|
|
2291
|
+
...written,
|
|
2292
|
+
excludedRanges: listExcludedRanges(handle),
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
|
|
1630
2296
|
function dispatchGetComments(handle: AnnoStoreHandle, args: unknown): unknown {
|
|
1631
2297
|
const maxResults = assertMaxResults("anno_get_comments", args);
|
|
1632
2298
|
const bag = argBag(args);
|
|
@@ -1737,6 +2403,273 @@ function dispatchSaveProject(handle: AnnoStoreHandle): unknown {
|
|
|
1737
2403
|
};
|
|
1738
2404
|
}
|
|
1739
2405
|
|
|
2406
|
+
/** Enforces `base_revision` as a whole-call precondition rather than
|
|
2407
|
+
* threading it through each of the many writes `importGhidraExport()` and
|
|
2408
|
+
* `runMemmapJoin()` may issue: both verbs commit several writes per call, and
|
|
2409
|
+
* a single up-front comparison against the revision the caller computed its
|
|
2410
|
+
* batch against is the coherent point to apply an optimistic-concurrency
|
|
2411
|
+
* guard for a multi-write verb -- checked BEFORE anything is written, exactly
|
|
2412
|
+
* like every other refusal on this surface. */
|
|
2413
|
+
function assertNotStale(name: string, handle: AnnoStoreHandle, baseRevision: number | undefined): void {
|
|
2414
|
+
if (baseRevision === undefined) return;
|
|
2415
|
+
const rev = currentRevision(handle);
|
|
2416
|
+
if (baseRevision !== rev) {
|
|
2417
|
+
throw new AnnoStoreStaleRevisionError(
|
|
2418
|
+
`${name} refused: base revision ${baseRevision} is not the current on-disk revision ${rev}. Nothing was written.`,
|
|
2419
|
+
{ baseRevision, currentRevision: rev },
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
function dispatchImportGhidraExport(handle: AnnoStoreHandle, args: unknown): unknown {
|
|
2425
|
+
const bag = argBag(args);
|
|
2426
|
+
const baseRevision = assertBaseRevisionArg("anno_import_ghidra_export", args);
|
|
2427
|
+
assertNotStale("anno_import_ghidra_export", handle, baseRevision);
|
|
2428
|
+
const exportPath = resolveExportPathArg(bag.export_path as string);
|
|
2429
|
+
return importGhidraExport(handle, {
|
|
2430
|
+
exportPath,
|
|
2431
|
+
expectedSha256: bag.sha256 as string | undefined,
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
/** `resolveWorkspacePath()` itself, never a second hand-rolled resolve-and-
|
|
2436
|
+
* prefix-test (T-37-01) -- but its underlying `AnnoStorePathError` message
|
|
2437
|
+
* says "store path ... is outside the workspace root", unaware of which
|
|
2438
|
+
* higher-level argument it was protecting, because `store` and `image` both
|
|
2439
|
+
* reuse the same generic wording. Wrapped here so a refusal on `export_path`
|
|
2440
|
+
* NAMES the argument rather than reading identically to a `store` refusal. */
|
|
2441
|
+
function resolveExportPathArg(raw: string): string {
|
|
2442
|
+
try {
|
|
2443
|
+
return resolveWorkspacePath(raw);
|
|
2444
|
+
} catch (err) {
|
|
2445
|
+
if (err instanceof AnnoStorePathError) {
|
|
2446
|
+
throw new AnnoStorePathError(`anno_import_ghidra_export refused: export_path ${err.message}`, {
|
|
2447
|
+
path: err.path,
|
|
2448
|
+
workspaceRoot: err.workspaceRoot,
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
throw err;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
function dispatchJoinMemmap(handle: AnnoStoreHandle, args: unknown): unknown {
|
|
2456
|
+
const baseRevision = assertBaseRevisionArg("anno_join_memmap", args);
|
|
2457
|
+
assertNotStale("anno_join_memmap", handle, baseRevision);
|
|
2458
|
+
const image = loadImage("anno_join_memmap", args);
|
|
2459
|
+
// CR-01 fix: `const_writes`/`graphics_map_index` are threaded into
|
|
2460
|
+
// `runMemmapJoin()` exactly as its own `RunMemmapJoinArgs` documents --
|
|
2461
|
+
// OMISSION (not `[]`) is what keeps every pre-existing call (no
|
|
2462
|
+
// const_writes at all) a byte-identical no-op for the bank-state and
|
|
2463
|
+
// graphics machinery.
|
|
2464
|
+
const constWrites = assertConstWritesArg("anno_join_memmap", args);
|
|
2465
|
+
const graphicsMapIndex = assertGraphicsMapIndexArg("anno_join_memmap", args);
|
|
2466
|
+
return runMemmapJoin(handle, {
|
|
2467
|
+
imageOrigin: image.origin,
|
|
2468
|
+
imageByteLength: image.body.length,
|
|
2469
|
+
...(constWrites !== undefined ? { constWrites } : {}),
|
|
2470
|
+
...(graphicsMapIndex !== undefined ? { graphicsMapIndex } : {}),
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
/**
|
|
2475
|
+
* `anno_evid_ingest`'s dispatch arm (EVID-01, EVID-04, plan 43-05). Calls
|
|
2476
|
+
* `parseAccessMap()` -- THE ONE PARSE -- then `ingestAccessMap()` from
|
|
2477
|
+
* `evid-ingest.ts`; on a refusal it throws inside the `ViceError` family
|
|
2478
|
+
* (never absorbs a drifted reply, T-43-22); on success it writes the WHOLE
|
|
2479
|
+
* observation array through ONE `insertExecObservations()` call, so the
|
|
2480
|
+
* write is one transaction through the store's single commit site
|
|
2481
|
+
* (T-43-26). `observationsWritten` is `insertExecObservations()`'s own
|
|
2482
|
+
* `insertedCount` (WR-02) -- counted row-by-row INSIDE that same
|
|
2483
|
+
* transaction, never from a separate pre-write read -- not the size of the
|
|
2484
|
+
* array handed in: re-ingesting the identical reply must report
|
|
2485
|
+
* `observationsWritten: 0` even though the same-shaped array was passed
|
|
2486
|
+
* again.
|
|
2487
|
+
*
|
|
2488
|
+
* `denominator` travels beside every count this answer reports
|
|
2489
|
+
* (`addressesQueried`, the parsed map's own projection) -- a bare
|
|
2490
|
+
* `observationsWritten` would invite the reading "the rest is data", which
|
|
2491
|
+
* is why the denominator is never omitted. No percentage is ever formed
|
|
2492
|
+
* here.
|
|
2493
|
+
*/
|
|
2494
|
+
function dispatchEvidIngest(handle: AnnoStoreHandle, args: unknown): unknown {
|
|
2495
|
+
const bag = argBag(args);
|
|
2496
|
+
const baseRevision = assertBaseRevisionArg("anno_evid_ingest", args);
|
|
2497
|
+
|
|
2498
|
+
const parsed = parseAccessMap(bag.memmap_text as string);
|
|
2499
|
+
const identity: IngestRunIdentity = {
|
|
2500
|
+
imageSha256: bag.image_sha256 as string,
|
|
2501
|
+
argv: bag.argv as string[],
|
|
2502
|
+
seed: bag.seed as string,
|
|
2503
|
+
};
|
|
2504
|
+
const ingested = ingestAccessMap(parsed, identity);
|
|
2505
|
+
if (!ingested.ok) {
|
|
2506
|
+
throw new AnnoToolArgumentError(`anno_evid_ingest refused: ${ingested.message}`, {
|
|
2507
|
+
toolName: "anno_evid_ingest",
|
|
2508
|
+
argument: "memmap_text",
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
const ranges = parsed.ok ? accessMapRanges(parsed.value) : undefined;
|
|
2513
|
+
const addressesWithRecordedAccess = ranges?.addressesWithRecordedAccess ?? 0;
|
|
2514
|
+
const addressesQueried = ranges?.addressesQueried ?? 0;
|
|
2515
|
+
|
|
2516
|
+
// A reply that recorded no execution anywhere is a real, legitimate
|
|
2517
|
+
// answer -- not an error -- but `insertExecObservations` refuses an EMPTY
|
|
2518
|
+
// observations array, so that zero-write case is reported directly here
|
|
2519
|
+
// rather than calling a store function built to refuse it. WR-01: it is
|
|
2520
|
+
// still routed through `applyWrite()` with a no-op mutator (rather than
|
|
2521
|
+
// returning early on `currentRevision(handle)` alone) so a stale
|
|
2522
|
+
// `base_revision` is refused on THIS path exactly as it would be on the
|
|
2523
|
+
// non-empty path below -- every other write verb in this store enforces
|
|
2524
|
+
// staleness through `applyWrite()`'s own check, and a caller relying on
|
|
2525
|
+
// that contract must not get a silent success here instead.
|
|
2526
|
+
if (ingested.observations.length === 0) {
|
|
2527
|
+
const { revision } = applyWrite(handle, () => false, { baseRevision });
|
|
2528
|
+
return {
|
|
2529
|
+
store: handle.path,
|
|
2530
|
+
revision,
|
|
2531
|
+
changed: false,
|
|
2532
|
+
observationsWritten: 0,
|
|
2533
|
+
addressesWithRecordedAccess,
|
|
2534
|
+
addressesQueried,
|
|
2535
|
+
denominator: addressesQueried,
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// WR-02: `observationsWritten` is the COUNT `insertExecObservations()`
|
|
2540
|
+
// itself returns, counted row-by-row INSIDE its own `applyWrite`
|
|
2541
|
+
// transaction -- never a `listExecObservations()` read taken before that
|
|
2542
|
+
// transaction opens. A separately-derived pre-read can be overtaken by a
|
|
2543
|
+
// concurrent writer to the same run identity between the read and this
|
|
2544
|
+
// call's own commit, overstating how many rows THIS call actually added;
|
|
2545
|
+
// counting inside the transaction that performs the insert is the one
|
|
2546
|
+
// place this number can be exact.
|
|
2547
|
+
const written = insertExecObservations(handle, {
|
|
2548
|
+
imageSha256: ingested.runIdentity.imageSha256,
|
|
2549
|
+
argvDigest: ingested.runIdentity.argvDigest,
|
|
2550
|
+
seed: ingested.runIdentity.seed,
|
|
2551
|
+
observations: ingested.observations.map((o) => ({ address: o.address, sourceBank: o.sourceBank })),
|
|
2552
|
+
baseRevision,
|
|
2553
|
+
});
|
|
2554
|
+
|
|
2555
|
+
return {
|
|
2556
|
+
store: handle.path,
|
|
2557
|
+
revision: written.revision,
|
|
2558
|
+
changed: written.changed,
|
|
2559
|
+
observationsWritten: written.insertedCount,
|
|
2560
|
+
addressesWithRecordedAccess,
|
|
2561
|
+
addressesQueried,
|
|
2562
|
+
denominator: addressesQueried,
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
/**
|
|
2567
|
+
* `anno_evid_disagreements`'s dispatch arm (EVID-03/EVID-04, plan 43-06).
|
|
2568
|
+
* Fetches BOTH sides HERE -- `listExecObservations()` and `listRanges()` --
|
|
2569
|
+
* so `reconcileObservedExecution()` (`evid-reconcile.ts`) is never handed a
|
|
2570
|
+
* store to open itself; that pure module's own header states it must never
|
|
2571
|
+
* fetch either side.
|
|
2572
|
+
*
|
|
2573
|
+
* The byte-derived ranges are mapped through `blocksFromStore()`, reached by
|
|
2574
|
+
* a LAZY `await import("./anno-cli.ts")` -- the same lazy pattern
|
|
2575
|
+
* `vice-proxy.ts:307` already uses to reach `runAnnoCli`, so this file's own
|
|
2576
|
+
* static import graph (and therefore the MCP server's startup cost) is
|
|
2577
|
+
* unchanged: `anno-cli.ts` drags in `anno-coverage.ts`, `anno-memmap-render.ts`
|
|
2578
|
+
* and `anno-export-asm.ts`, none of which this verb needs. The mapping
|
|
2579
|
+
* itself is NOT re-implemented here: a second `RangeRow` -> `BlockEntry` site
|
|
2580
|
+
* would be a second answer to "what class is this address", which is
|
|
2581
|
+
* exactly the boundary `block-class.ts` (and `blocksFromStore()`'s own
|
|
2582
|
+
* comment) exists to keep at one.
|
|
2583
|
+
*
|
|
2584
|
+
* `max_results` (optional, `assertOptionalMaxResults`) bounds the RETURNED
|
|
2585
|
+
* `disagreements` array only -- `agreementCount` and every other bucket are
|
|
2586
|
+
* already counts, never rows, so there is nothing else to truncate.
|
|
2587
|
+
* `reconciliation`'s own key order is preserved by spreading it before
|
|
2588
|
+
* re-assigning `disagreements`: JS does not move an existing key to the end
|
|
2589
|
+
* of an object literal on reassignment, so `disagreements` stays the FIRST
|
|
2590
|
+
* key after `store` (EVID-03).
|
|
2591
|
+
*/
|
|
2592
|
+
async function dispatchEvidDisagreements(handle: AnnoStoreHandle, args: unknown): Promise<unknown> {
|
|
2593
|
+
const maxResults = assertOptionalMaxResults("anno_evid_disagreements", args);
|
|
2594
|
+
const bag = argBag(args);
|
|
2595
|
+
const hasRunFilter = bag.image_sha256 !== undefined;
|
|
2596
|
+
const observations = listExecObservations(
|
|
2597
|
+
handle,
|
|
2598
|
+
hasRunFilter ? { imageSha256: bag.image_sha256, argvDigest: bag.argv_digest, seed: bag.seed } : {},
|
|
2599
|
+
);
|
|
2600
|
+
// Lazy, deliberately: see this function's own doc comment above for why a
|
|
2601
|
+
// static top-level import of anno-cli.ts must never appear in this file.
|
|
2602
|
+
const { blocksFromStore } = await import("./anno-cli.ts");
|
|
2603
|
+
const blocks = blocksFromStore(listRanges(handle));
|
|
2604
|
+
const reconciliation = reconcileObservedExecution({ blocks, observations });
|
|
2605
|
+
const disagreements = maxResults === undefined ? reconciliation.disagreements : reconciliation.disagreements.slice(0, maxResults);
|
|
2606
|
+
return {
|
|
2607
|
+
store: handle.path,
|
|
2608
|
+
...reconciliation,
|
|
2609
|
+
disagreements,
|
|
2610
|
+
returned: disagreements.length,
|
|
2611
|
+
matched: reconciliation.disagreements.length,
|
|
2612
|
+
truncated: reconciliation.disagreements.length > disagreements.length,
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
/** `anno_evid_runs`'s dispatch arm (plan 43-06): `listObservedRuns()`'s own
|
|
2617
|
+
* answer, carried through UNCHANGED beside `store` -- its `denominator` is
|
|
2618
|
+
* reported exactly as that function computed it, never re-derived here. */
|
|
2619
|
+
function dispatchEvidRuns(handle: AnnoStoreHandle, args: unknown): unknown {
|
|
2620
|
+
void args; // this verb takes no argument beyond the universal `store`
|
|
2621
|
+
return { store: handle.path, ...listObservedRuns(handle) };
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
/**
|
|
2625
|
+
* `anno_evid_reset`'s dispatch arm (EVID-05, plan 43-06): the store-side
|
|
2626
|
+
* half of a bracket reset, beside plan 43-03's emulator-side
|
|
2627
|
+
* `vice_memmap_zap`. Derives the run identity through `runIdentityFrom()`
|
|
2628
|
+
* from `evid-ingest.ts` -- the SAME single digest site `anno_evid_ingest`
|
|
2629
|
+
* uses -- never a second hashing site here, and never a caller-supplied
|
|
2630
|
+
* digest. `observationsRemoved` is read from a `listExecObservations()`
|
|
2631
|
+
* query taken BEFORE the delete, so the answer names exactly how many rows
|
|
2632
|
+
* this call removed rather than leaving a caller to infer it from `changed`
|
|
2633
|
+
* alone. `baseRevision` is threaded straight into
|
|
2634
|
+
* `deleteExecObservationsForRun()`, which enforces staleness itself through
|
|
2635
|
+
* `applyWrite()` -- the same "let the store's own write sequence check it"
|
|
2636
|
+
* discipline `dispatchEvidIngest()` above already uses, so there is no
|
|
2637
|
+
* second, redundant `assertNotStale()` call here.
|
|
2638
|
+
*/
|
|
2639
|
+
function dispatchEvidReset(handle: AnnoStoreHandle, args: unknown): unknown {
|
|
2640
|
+
const bag = argBag(args);
|
|
2641
|
+
const baseRevision = assertBaseRevisionArg("anno_evid_reset", args);
|
|
2642
|
+
const identity = runIdentityFrom({
|
|
2643
|
+
imageSha256: bag.image_sha256 as string,
|
|
2644
|
+
argv: bag.argv as string[],
|
|
2645
|
+
seed: bag.seed as string,
|
|
2646
|
+
});
|
|
2647
|
+
const existing = listExecObservations(handle, {
|
|
2648
|
+
imageSha256: identity.imageSha256,
|
|
2649
|
+
argvDigest: identity.argvDigest,
|
|
2650
|
+
seed: identity.seed,
|
|
2651
|
+
});
|
|
2652
|
+
const written = deleteExecObservationsForRun(handle, {
|
|
2653
|
+
imageSha256: identity.imageSha256,
|
|
2654
|
+
argvDigest: identity.argvDigest,
|
|
2655
|
+
seed: identity.seed,
|
|
2656
|
+
baseRevision,
|
|
2657
|
+
});
|
|
2658
|
+
return {
|
|
2659
|
+
store: handle.path,
|
|
2660
|
+
revision: written.revision,
|
|
2661
|
+
changed: written.changed,
|
|
2662
|
+
observationsRemoved: existing.length,
|
|
2663
|
+
// `denominator` travels beside `observationsRemoved` for the same reason
|
|
2664
|
+
// it travels beside every other count this evidence layer reports
|
|
2665
|
+
// (EVID-04, plan 43-07's own structural guard): a bare count invites the
|
|
2666
|
+
// reading "the rest is data". The bracket this call reset held exactly
|
|
2667
|
+
// `existing.length` rows before the delete, so that is what
|
|
2668
|
+
// `observationsRemoved` is a fraction of -- a full reset makes the two
|
|
2669
|
+
// numbers equal, but the field is never omitted just because it agrees.
|
|
2670
|
+
denominator: existing.length,
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
1740
2673
|
|
|
1741
2674
|
// ---------------------------------------------------------------------------
|
|
1742
2675
|
// The image loader (D-07). The store holds annotations and never bytes, so
|
|
@@ -1871,7 +2804,175 @@ function hexdump(bytes: Uint8Array, start: number): string[] {
|
|
|
1871
2804
|
return lines;
|
|
1872
2805
|
}
|
|
1873
2806
|
|
|
1874
|
-
|
|
2807
|
+
/** The SAME shape `registerKeyFor().slice(1)` produces (uppercase, exactly
|
|
2808
|
+
* four hex digits) -- `anno-export-asm.ts`'s own `REGISTER_ENUM_NAME_RE`
|
|
2809
|
+
* comment explains why an enum usage is only a CANDIDATE for the decoder
|
|
2810
|
+
* when its name has this shape, and why that check is not centralised: two
|
|
2811
|
+
* renderers, two small local copies of this one shape predicate, one shared
|
|
2812
|
+
* decoder. Kept in sync by inspection (both are one line) rather than by
|
|
2813
|
+
* import, per D-16's own "two renderers" design.
|
|
2814
|
+
*
|
|
2815
|
+
* SHAPE ALONE IS NOT ENOUGH (45-REVIEW CR-01, fixed 2026-09-11): the call
|
|
2816
|
+
* site below also requires `hasRegBitsEntry()` -- imported from
|
|
2817
|
+
* `anno-enum-gen.ts` above, the ONE shared membership predicate, NOT a third
|
|
2818
|
+
* local copy -- to confirm `anno-regbits.json` actually covers the register
|
|
2819
|
+
* before attempting the decoder at all. */
|
|
2820
|
+
const REGISTER_ENUM_NAME_RE = /^[0-9A-F]{4}$/;
|
|
2821
|
+
|
|
2822
|
+
/** `#$XX` -> `#<replacement>` on the ASSEMBLER-VISIBLE half of `line`, the
|
|
2823
|
+
* same confinement `anno-export-asm.ts`'s `substituteImmediateEnum()` uses
|
|
2824
|
+
* (never rewriting inside a trailing `;` comment, where a renderer's own
|
|
2825
|
+
* NOTE text could coincidentally contain the same hex digits). A rendered
|
|
2826
|
+
* line that does not carry the expected literal is a disagreement between
|
|
2827
|
+
* this function and `disasm-renderer.ts`, and it is refused rather than
|
|
2828
|
+
* silently left unchanged. */
|
|
2829
|
+
function substituteReadableImmediate(line: string, value: number, replacement: string, address: number): string {
|
|
2830
|
+
const literal = `#$${(value & 0xff).toString(16).padStart(2, "0")}`;
|
|
2831
|
+
const separatorIndex = line.indexOf(" ; ");
|
|
2832
|
+
const directiveHalf = separatorIndex >= 0 ? line.slice(0, separatorIndex) : line;
|
|
2833
|
+
const commentHalf = separatorIndex >= 0 ? line.slice(separatorIndex) : "";
|
|
2834
|
+
const at = directiveHalf.indexOf(literal);
|
|
2835
|
+
if (at < 0) {
|
|
2836
|
+
throw new AnnoStoreError(
|
|
2837
|
+
`anno_disassemble: the instruction at $${address.toString(16).padStart(4, "0")} carries an enum usage, but its rendered line does ` +
|
|
2838
|
+
`not contain the immediate literal ${literal} this renderer expected to replace. Refusing rather than emitting a line whose ` +
|
|
2839
|
+
"substitution silently did nothing.",
|
|
2840
|
+
);
|
|
2841
|
+
}
|
|
2842
|
+
return `${directiveHalf.slice(0, at)}#${replacement}${directiveHalf.slice(at + literal.length)}${commentHalf}`;
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
/** Appends `comment` as a trailing `;`-comment on `line`, joining it with any
|
|
2846
|
+
* EXISTING trailing comment (a `disasm-renderer.ts` note, e.g. an NMOS
|
|
2847
|
+
* page-wrap warning) via `" | "` -- the SAME separator `formatNotesComment()`
|
|
2848
|
+
* already uses to join multiple notes on one instruction, so a line with
|
|
2849
|
+
* both a note and a decoded register comment reads as one vocabulary rather
|
|
2850
|
+
* than two different join styles on one line. */
|
|
2851
|
+
function appendReadableComment(line: string, comment: string): string {
|
|
2852
|
+
const separatorIndex = line.indexOf(" ; ");
|
|
2853
|
+
if (separatorIndex < 0) return `${line} ; ${comment}`;
|
|
2854
|
+
return `${line} | ${comment}`;
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
/**
|
|
2858
|
+
* D-16's SECOND renderer (plan 45-05): the READABILITY half. `anno-export-
|
|
2859
|
+
* asm.ts` carries the proof (a real-ACME byte-diff oracle); this is what a
|
|
2860
|
+
* Claude session actually reads. Calls `decomposeRegisterValue()` -- the ONE
|
|
2861
|
+
* owning decoder -- for exactly the same reason: this function decodes
|
|
2862
|
+
* NOTHING itself.
|
|
2863
|
+
*
|
|
2864
|
+
* BYTE-IDENTICAL TO `render()`'S OWN OUTPUT when the store carries no enum
|
|
2865
|
+
* usage inside the decoded range at all (the fast-path return below), and
|
|
2866
|
+
* for every instruction `usageByAddress` does not cover even when it does --
|
|
2867
|
+
* D-16 widens what a bound instruction shows; it does not touch anything
|
|
2868
|
+
* else `render()` already produces.
|
|
2869
|
+
*
|
|
2870
|
+
* THE LINE-INDEX MAPPING THIS RELIES ON: `render(instructions, { origin })`
|
|
2871
|
+
* is called here WITHOUT `showSymbols`, so `resolveSymbol()` (`disasm-
|
|
2872
|
+
* renderer.ts`) always returns `undefined` and its own symbol-header loop
|
|
2873
|
+
* never emits a line -- the header is EXACTLY `"!cpu 6510"` then `"* =
|
|
2874
|
+
* $XXXX"`, two lines, and `instructions[i]` maps to `lines[HEADER_LINES +
|
|
2875
|
+
* i]` with no other possible offset. A future caller of this function that
|
|
2876
|
+
* ever passes `showSymbols: true` would break that mapping silently; this
|
|
2877
|
+
* function does not, and does not need to for the readability job D-16 gives
|
|
2878
|
+
* it.
|
|
2879
|
+
*/
|
|
2880
|
+
function renderDisassembleListing(handle: AnnoStoreHandle, instructions: readonly Instruction[], origin: number): string {
|
|
2881
|
+
const baseListing = render(instructions as Instruction[], { origin });
|
|
2882
|
+
|
|
2883
|
+
const usageByAddress = new Map<number, EnumUsageRow>();
|
|
2884
|
+
for (const row of listEnumUsage(handle)) usageByAddress.set(row.address, row);
|
|
2885
|
+
if (usageByAddress.size === 0) return baseListing;
|
|
2886
|
+
|
|
2887
|
+
const enumsByName = new Map<string, ProjectEnumRow>();
|
|
2888
|
+
for (const row of listProjectEnums(handle)) enumsByName.set(row.name, row);
|
|
2889
|
+
|
|
2890
|
+
const HEADER_LINES = 2;
|
|
2891
|
+
const lines = baseListing.split("\n");
|
|
2892
|
+
|
|
2893
|
+
instructions.forEach((instr, index) => {
|
|
2894
|
+
const usage = usageByAddress.get(instr.address);
|
|
2895
|
+
if (usage === undefined) return;
|
|
2896
|
+
|
|
2897
|
+
// THE SAME REFUSAL SHAPE THE EXPORT BOUNDARY RAISES (`anno-export-
|
|
2898
|
+
// asm.ts`'s own enum-substitution block) for the same conditions, not a
|
|
2899
|
+
// silently plain listing for a store row this readable surface cannot
|
|
2900
|
+
// honour.
|
|
2901
|
+
const project = enumsByName.get(usage.enumName);
|
|
2902
|
+
if (project === undefined) {
|
|
2903
|
+
throw new AnnoStoreError(
|
|
2904
|
+
`anno_disassemble: the enum usage at $${instr.address.toString(16).padStart(4, "0")} names enum ${JSON.stringify(usage.enumName)}, ` +
|
|
2905
|
+
"which the store holds no definition for. Refusing to render a readable operand whose vocabulary is missing.",
|
|
2906
|
+
);
|
|
2907
|
+
}
|
|
2908
|
+
const role = instr.operand?.role;
|
|
2909
|
+
if (role !== "immediate" || !instr.acmeExpressible) {
|
|
2910
|
+
throw new AnnoStoreError(
|
|
2911
|
+
`anno_disassemble: the enum usage at $${instr.address.toString(16).padStart(4, "0")} names enum ${JSON.stringify(usage.enumName)}, but ` +
|
|
2912
|
+
"the instruction there is not an assembler-visible IMMEDIATE operand -- an enum renders on the immediate operand only. Refusing " +
|
|
2913
|
+
"rather than rendering a readable line with no substitution.",
|
|
2914
|
+
);
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
// D-16: attempted ONLY when BOTH (45-REVIEW CR-01, fixed 2026-09-11) the
|
|
2918
|
+
// enum's name has the register-key shape -- see `REGISTER_ENUM_NAME_RE`'s
|
|
2919
|
+
// own comment for why a name that does not (e.g. a hand-authored
|
|
2920
|
+
// `viccolor`) is never a candidate -- AND `anno-regbits.json` actually
|
|
2921
|
+
// has a table entry for it (`hasRegBitsEntry()`). A register-shaped name
|
|
2922
|
+
// for a register the table does not cover (e.g. `D020`) is not a
|
|
2923
|
+
// decomposition failure; it falls through to the single-symbol shape
|
|
2924
|
+
// below with no decomposition attempted at all.
|
|
2925
|
+
let decomposition: RegisterDecomposition | undefined;
|
|
2926
|
+
if (REGISTER_ENUM_NAME_RE.test(usage.enumName) && hasRegBitsEntry(`$${usage.enumName}`)) {
|
|
2927
|
+
try {
|
|
2928
|
+
// `Number("0x...")`, never `parseInt()` -- this file's own guard
|
|
2929
|
+
// (anno-tools.test.ts) forbids a second, divergent numeric-parsing
|
|
2930
|
+
// rule beside the store's own. `usage.enumName` is already proven
|
|
2931
|
+
// to match REGISTER_ENUM_NAME_RE (four hex digits) above.
|
|
2932
|
+
decomposition = decomposeRegisterValue(Number(`0x${usage.enumName}`), instr.operand!.value);
|
|
2933
|
+
} catch (err) {
|
|
2934
|
+
throw new AnnoStoreError(
|
|
2935
|
+
`anno_disassemble: decomposing the enum usage at $${instr.address.toString(16).padStart(4, "0")} (enum ` +
|
|
2936
|
+
`${JSON.stringify(usage.enumName)}) against its bit-name table failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2937
|
+
);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
const lineIndex = HEADER_LINES + index;
|
|
2942
|
+
const currentLine = lines[lineIndex]!;
|
|
2943
|
+
|
|
2944
|
+
if (decomposition !== undefined && decomposition.multiField) {
|
|
2945
|
+
// D-17: OR-ed named constants AND the decoded comment -- both, exactly
|
|
2946
|
+
// as the export renders them, so a Claude session reading this listing
|
|
2947
|
+
// sees what the export proves.
|
|
2948
|
+
const orExpression = decomposition.terms.map((term) => term.name).join(" | ");
|
|
2949
|
+
const substituted = substituteReadableImmediate(currentLine, instr.operand!.value, orExpression, instr.address);
|
|
2950
|
+
lines[lineIndex] = appendReadableComment(substituted, decomposition.comment);
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
// THE EXISTING SINGLE-SYMBOL SHAPE (D-16: not replaced) -- a single-field
|
|
2955
|
+
// register, an enum usage whose name is not register-shaped at all, OR
|
|
2956
|
+
// (45-REVIEW CR-01) a register-shaped name for a register
|
|
2957
|
+
// `anno-regbits.json` has no entry for (e.g. `D020`).
|
|
2958
|
+
let matched: string | undefined;
|
|
2959
|
+
for (const [key, variantName] of Object.entries(project.variants)) {
|
|
2960
|
+
if (parseVariantKey(key) === instr.operand!.value) matched = variantName;
|
|
2961
|
+
}
|
|
2962
|
+
if (matched === undefined) {
|
|
2963
|
+
throw new AnnoStoreError(
|
|
2964
|
+
`anno_disassemble: enum ${JSON.stringify(usage.enumName)} is bound to the immediate operand at ` +
|
|
2965
|
+
`$${instr.address.toString(16).padStart(4, "0")}, whose value is $${(instr.operand!.value & 0xff).toString(16).padStart(2, "0")}, ` +
|
|
2966
|
+
"and the enum has no variant for that value.",
|
|
2967
|
+
);
|
|
2968
|
+
}
|
|
2969
|
+
lines[lineIndex] = substituteReadableImmediate(currentLine, instr.operand!.value, `${usage.enumName}_${matched}`, instr.address);
|
|
2970
|
+
});
|
|
2971
|
+
|
|
2972
|
+
return lines.join("\n");
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
function dispatchDisassemble(handle: AnnoStoreHandle, args: unknown): unknown {
|
|
1875
2976
|
const image = loadImage("anno_disassemble", args);
|
|
1876
2977
|
const bag = argBag(args);
|
|
1877
2978
|
const start = parseStoreAddress(bag.address, { what: "address" });
|
|
@@ -1897,7 +2998,51 @@ function dispatchDisassemble(args: unknown): unknown {
|
|
|
1897
2998
|
address: start,
|
|
1898
2999
|
end_address: requestedEnd,
|
|
1899
3000
|
instructions: instructions.length,
|
|
1900
|
-
listing:
|
|
3001
|
+
listing: renderDisassembleListing(handle, instructions, start),
|
|
3002
|
+
};
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
/**
|
|
3006
|
+
* `anno_hazard_report`'s dispatch arm. Fetches EVERY input here -- the
|
|
3007
|
+
* byte-derived ranges, labels, comments, cross-references, execution
|
|
3008
|
+
* observations and the image bytes -- and hands them to `buildHazardReport()`
|
|
3009
|
+
* exactly once; the pure module itself never fetches any of it (see its own
|
|
3010
|
+
* header). The byte-derived ranges are mapped through `blocksFromStore()`,
|
|
3011
|
+
* reached by the SAME lazy `await import("./anno-cli.ts")`
|
|
3012
|
+
* `dispatchEvidDisagreements()` already uses above, so this file's own static
|
|
3013
|
+
* import graph -- and therefore the MCP server's startup cost -- stays
|
|
3014
|
+
* unchanged: `anno-cli.ts` drags in `anno-coverage.ts`, `anno-memmap-render.ts`
|
|
3015
|
+
* and `anno-export-asm.ts`, none of which this verb needs either. The mapping
|
|
3016
|
+
* itself is NOT re-implemented here, for the same reason `dispatchEvidDisagreements`
|
|
3017
|
+
* states for itself.
|
|
3018
|
+
*/
|
|
3019
|
+
async function dispatchHazardReport(handle: AnnoStoreHandle, args: unknown): Promise<unknown> {
|
|
3020
|
+
const maxResults = assertOptionalMaxResults("anno_hazard_report", args);
|
|
3021
|
+
const image = loadImage("anno_hazard_report", args);
|
|
3022
|
+
const { blocksFromStore } = await import("./anno-cli.ts");
|
|
3023
|
+
const ranges = blocksFromStore(listRanges(handle));
|
|
3024
|
+
const symbols = listLabels(handle);
|
|
3025
|
+
const comments = listComments(handle);
|
|
3026
|
+
const xrefs = listXrefs(handle);
|
|
3027
|
+
const execObservations = listExecObservations(handle);
|
|
3028
|
+
const report = buildHazardReport({
|
|
3029
|
+
bytes: image.body,
|
|
3030
|
+
origin: image.origin,
|
|
3031
|
+
symbols,
|
|
3032
|
+
comments,
|
|
3033
|
+
ranges,
|
|
3034
|
+
xrefs,
|
|
3035
|
+
execObservations,
|
|
3036
|
+
});
|
|
3037
|
+
const findings = maxResults === undefined ? report.findings : report.findings.slice(0, maxResults);
|
|
3038
|
+
return {
|
|
3039
|
+
store: handle.path,
|
|
3040
|
+
image: image.path,
|
|
3041
|
+
...report,
|
|
3042
|
+
findings,
|
|
3043
|
+
returned: findings.length,
|
|
3044
|
+
matched: report.findings.length,
|
|
3045
|
+
truncated: report.truncated || report.findings.length > findings.length,
|
|
1901
3046
|
};
|
|
1902
3047
|
}
|
|
1903
3048
|
|
|
@@ -2043,13 +3188,21 @@ async function dispatch(name: string, args: unknown, handle: AnnoStoreHandle): P
|
|
|
2043
3188
|
if (name === "anno_set_comment") return dispatchSetComment(handle, args);
|
|
2044
3189
|
if (name === "anno_set_data_type") return dispatchSetDataType(handle, args);
|
|
2045
3190
|
if (name === "anno_add_scope" || name === "anno_remove_scope") return dispatchScope(name, handle, args);
|
|
3191
|
+
if (name === "anno_exclude_range" || name === "anno_include_range") return dispatchExcludedRange(name, handle, args);
|
|
2046
3192
|
if (name === "anno_get_comments") return dispatchGetComments(handle, args);
|
|
2047
3193
|
if (name === "anno_get_blocks") return dispatchGetBlocks(handle, args);
|
|
2048
3194
|
if (name === "anno_create_project_enum") return dispatchCreateProjectEnum(handle, args);
|
|
2049
3195
|
if (name === "anno_update_project_enum") return dispatchUpdateProjectEnum(handle, args);
|
|
2050
3196
|
if (name === "anno_apply_enum_usage") return dispatchApplyEnumUsage(handle, args);
|
|
2051
3197
|
if (name === "anno_save_project") return dispatchSaveProject(handle);
|
|
2052
|
-
if (name === "
|
|
3198
|
+
if (name === "anno_import_ghidra_export") return dispatchImportGhidraExport(handle, args);
|
|
3199
|
+
if (name === "anno_join_memmap") return dispatchJoinMemmap(handle, args);
|
|
3200
|
+
if (name === "anno_evid_ingest") return dispatchEvidIngest(handle, args);
|
|
3201
|
+
if (name === "anno_evid_disagreements") return dispatchEvidDisagreements(handle, args);
|
|
3202
|
+
if (name === "anno_evid_runs") return dispatchEvidRuns(handle, args);
|
|
3203
|
+
if (name === "anno_evid_reset") return dispatchEvidReset(handle, args);
|
|
3204
|
+
if (name === "anno_disassemble") return dispatchDisassemble(handle, args);
|
|
3205
|
+
if (name === "anno_hazard_report") return dispatchHazardReport(handle, args);
|
|
2053
3206
|
if (name === "anno_read_region") return dispatchReadRegion(args);
|
|
2054
3207
|
if (name === "anno_get_binary_info") return dispatchBinaryInfo(args);
|
|
2055
3208
|
if (name === "anno_get_cross_references") return dispatchCrossReferences(handle, args);
|