@henols/c64-re-tools 1.1.0 → 2.0.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -37
  3. package/distribution/package.ts +21 -0
  4. package/distribution/plugin.ts +122 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +76 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +110 -0
  8. package/skills/c64-assembler/scripts/compare.ts +84 -0
  9. package/skills/c64-basic/SKILL.md +66 -0
  10. package/skills/c64-basic/scripts/basic.ts +46 -0
  11. package/skills/c64-disk/SKILL.md +93 -0
  12. package/skills/c64-disk/scripts/containers.ts +124 -0
  13. package/skills/c64-disk/scripts/disk.ts +120 -0
  14. package/skills/c64-emulator/SKILL.md +134 -0
  15. package/skills/c64-knowledge/SKILL.md +166 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +492 -0
  17. package/skills/c64-memory-map/SKILL.md +79 -0
  18. package/skills/c64-memory-map/scripts/memmap.ts +92 -0
  19. package/skills/c64-provenance/SKILL.md +110 -0
  20. package/skills/c64-provenance/scripts/compare.ts +152 -0
  21. package/skills/c64-provenance/scripts/provenance.ts +334 -0
  22. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  23. package/skills/c64-provenance/scripts/separate.ts +140 -0
  24. package/skills/c64-reverse-engineering/SKILL.md +144 -0
  25. package/skills/c64-source/SKILL.md +126 -0
  26. package/skills/c64-source/scripts/generate.ts +490 -0
  27. package/skills/c64-source/scripts/loader.ts +571 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +595 -0
  30. package/skills/c64-static-analysis/SKILL.md +119 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +275 -0
  32. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  33. package/skills/c64-static-analysis/scripts/findings.ts +75 -0
  34. package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
  35. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  36. package/skills/c64-testing/SKILL.md +103 -0
  37. package/skills/c64-testing/references/scenario-format.md +116 -0
  38. package/skills/c64-testing/scripts/checklist.ts +51 -0
  39. package/skills/c64-testing/scripts/run.ts +189 -0
  40. package/skills/c64-testing/scripts/scenario.ts +493 -0
  41. package/skills/c64-testing/scripts/test.ts +65 -0
  42. package/skills/c64-unpacker/SKILL.md +135 -0
  43. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  44. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  45. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  46. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  47. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  48. package/src/c64.ts +411 -0
  49. package/src/cli/ap-sdk.ts +44 -0
  50. package/src/cli/cleanup.ts +194 -0
  51. package/src/cli/main.ts +125 -0
  52. package/src/cli/status.ts +61 -0
  53. package/src/host/main.ts +134 -0
  54. package/src/host/server.ts +392 -0
  55. package/src/host/tools/c1541.ts +284 -0
  56. package/src/host/tools/index.ts +35 -0
  57. package/src/host/tools/petcat.ts +77 -0
  58. package/src/host/tools/status.ts +47 -0
  59. package/src/host/tools/version.ts +41 -0
  60. package/src/host/vice/adapter.ts +804 -0
  61. package/src/host/vice/binary-monitor.ts +389 -0
  62. package/src/host/vice/chips.ts +118 -0
  63. package/src/host/vice/connect.ts +34 -0
  64. package/src/host/vice/process.ts +413 -0
  65. package/src/host/vice/screen.ts +184 -0
  66. package/src/host/vice/session.ts +1426 -0
  67. package/src/host/vice/text-monitor.ts +223 -0
  68. package/src/host-client/autostart.ts +55 -0
  69. package/src/host-client/connect.ts +241 -0
  70. package/src/host-client/coverage.ts +63 -0
  71. package/src/host-client/steps.ts +170 -0
  72. package/src/host-client/tools.ts +89 -0
  73. package/src/host-client/transfer.ts +126 -0
  74. package/src/host-client/vice-session.ts +316 -0
  75. package/src/knowledge/database.ts +132 -0
  76. package/src/knowledge/history.ts +163 -0
  77. package/src/knowledge/import.ts +391 -0
  78. package/src/knowledge/read.ts +342 -0
  79. package/src/knowledge/schema.ts +229 -0
  80. package/src/knowledge/write.ts +663 -0
  81. package/src/mcp/main.ts +134 -0
  82. package/src/mcp/output.ts +32 -0
  83. package/src/mcp/server.ts +316 -0
  84. package/src/mcp/tools/debug.ts +327 -0
  85. package/src/mcp/tools/execution.ts +117 -0
  86. package/src/mcp/tools/input.ts +72 -0
  87. package/src/mcp/tools/machine.ts +100 -0
  88. package/src/mcp/tools/media.ts +129 -0
  89. package/src/mcp/tools/memory.ts +250 -0
  90. package/src/mcp/tools/video.ts +271 -0
  91. package/src/native/acme.ts +116 -0
  92. package/src/native/discover.ts +179 -0
  93. package/src/native/dxa.ts +166 -0
  94. package/src/native/ghidra/analyze.ts +86 -0
  95. package/src/native/ghidra/index.ts +173 -0
  96. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  97. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  98. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  99. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  100. package/src/native/ghidra/scripts/C64Export.java +274 -0
  101. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  102. package/src/native/local.ts +34 -0
  103. package/src/native/processes.ts +350 -0
  104. package/src/native/run.ts +172 -0
  105. package/src/native/staging.ts +83 -0
  106. package/src/native/status.ts +113 -0
  107. package/src/native/types.ts +257 -0
  108. package/src/native/watchdog.ts +78 -0
  109. package/src/package.ts +21 -0
  110. package/src/platform.ts +441 -0
  111. package/src/project.ts +39 -0
  112. package/src/protocol/framing.ts +132 -0
  113. package/src/protocol/messages.ts +259 -0
  114. package/src/protocol/tools.ts +266 -0
  115. package/src/protocol/vice.ts +1348 -0
  116. package/src/script.ts +60 -0
  117. package/src/time.ts +15 -0
  118. package/src/trace.ts +194 -0
  119. package/THIRD-PARTY-NOTICES.md +0 -26
  120. package/bin/cli.mjs +0 -283
  121. package/skills/acme-build/SKILL.md +0 -260
  122. package/skills/acme-build/scripts/acme.mjs +0 -341
  123. package/skills/acme-build/template.a +0 -53
  124. package/skills/c64-disk-access/SKILL.md +0 -159
  125. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  126. package/skills/c64-memory-mapping/SKILL.md +0 -631
  127. package/skills/c64-memory-mapping/memmap.json +0 -8800
  128. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  129. package/skills/c64-petcat/SKILL.md +0 -87
  130. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  131. package/skills/c64-program-recon/SKILL.md +0 -706
  132. package/skills/c64-program-recon/references/control-flow.md +0 -180
  133. package/skills/c64-program-recon/references/graphics.md +0 -79
  134. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  135. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  136. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  137. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  138. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  139. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  140. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  141. package/skills/c64-provenance-diff/SKILL.md +0 -292
  142. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  143. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  144. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  145. package/skills/c64-ram-capture/SKILL.md +0 -474
  146. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  147. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  148. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  149. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  150. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  151. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  152. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  153. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  154. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  155. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  156. package/skills/c64-ram-capture/transients/README.md +0 -137
  157. package/skills/routine-queue-walker/SKILL.md +0 -394
  158. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  159. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -0,0 +1,391 @@
1
+ // Analyzer import: reconciles one analyzer's normalized findings with
2
+ // current knowledge as one revision in one transaction.
3
+ //
4
+ // An import is a snapshot of what the analyzer asserts inside its coverage.
5
+ // In an authoritative category, the analyzer's own current rows in coverage
6
+ // that the snapshot no longer holds are closed. Rows of the user, the LLM or
7
+ // another analyzer are never replaced: a finding that contradicts one is
8
+ // reported as a conflict. Code against data is a contradiction; a different
9
+ // name or a finer data type for the same kind of memory is not. One exception
10
+ // (D58): where a run executed an instruction start, a code finding replaces
11
+ // the data of another analyzer, never of the user, the LLM or a heuristic.
12
+
13
+ import { createHash } from "node:crypto";
14
+ import type { DatabaseSync } from "node:sqlite";
15
+
16
+ import { KnowledgeError, transaction } from "./database.ts";
17
+ import {
18
+ REFERENCE_COLUMNS,
19
+ referenceSuppressed,
20
+ regionsOverlapping,
21
+ suppressionsOverlapping,
22
+ SYMBOL_COLUMNS,
23
+ symbolAt,
24
+ symbolNamed,
25
+ symbolSuppressed,
26
+ toReference,
27
+ toSymbol,
28
+ type ReferenceRow,
29
+ type RegionRow,
30
+ type SymbolRow,
31
+ } from "./read.ts";
32
+ import { CODE_REGION_TYPES, DEFAULT_SPACE, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
33
+ import { Change, checkAddress, checkSpace, hex, subtract, SYMBOL_NAME, type AddressRange, type Evidence } from "./write.ts";
34
+
35
+ export type { AddressRange };
36
+
37
+ export const ANALYZERS = ["dxa", "ghidra", "heuristic"] as const satisfies readonly Origin[];
38
+ export type Analyzer = (typeof ANALYZERS)[number];
39
+
40
+ export interface NormalizedFindings {
41
+ analyzer: Analyzer;
42
+ /** The address space that the analyzed image belongs to (D53); the C64 when absent. */
43
+ space?: Space;
44
+ /** The address ranges the analyzer examined; it makes claims only there. */
45
+ coverage: AddressRange[];
46
+ /** A category is authoritative when its findings are complete in coverage, so absence means "no longer asserted". */
47
+ authoritative: { symbols: boolean; regions: boolean; references: boolean };
48
+ symbols: Array<{ address: number; name: string; kind: SymbolKind }>;
49
+ regions: Array<{ start: number; end: number; type: RegionType }>;
50
+ /** Owned and covered by their source address. */
51
+ references: Array<{ from: number; to: number; kind: ReferenceKind }>;
52
+ /** True for an address where a coverage run executed an instruction start (D58). */
53
+ executed?: (address: number) => boolean;
54
+ }
55
+
56
+ export interface ImportContext {
57
+ /** The revision the caller read; the import is refused if knowledge changed since. */
58
+ expectedRevision?: number;
59
+ description?: string;
60
+ /** The SHA-256 of the analyzed image, as lowercase hex. */
61
+ inputHash?: string;
62
+ /** The analyzer's own version line. */
63
+ toolVersion?: string;
64
+ /** What the import rests on besides the image, for example the coverage files. */
65
+ evidence?: Evidence[];
66
+ }
67
+
68
+ /** What a revision records about one analyzer run: the hash of the image bytes and, when the analyzer tells it, its version. */
69
+ export function runProvenance(image: Uint8Array, toolVersion: string | undefined): Pick<ImportContext, "inputHash" | "toolVersion"> {
70
+ const inputHash = createHash("sha256").update(image).digest("hex");
71
+ return toolVersion === undefined ? { inputHash } : { inputHash, toolVersion };
72
+ }
73
+
74
+ export type ImportConflict =
75
+ | {
76
+ category: "symbol";
77
+ address: number;
78
+ reason: "kind" | "name-taken";
79
+ current: { address: number; name: string; kind: SymbolKind; origin: Origin };
80
+ finding: { name: string; kind: SymbolKind };
81
+ }
82
+ | { category: "region"; start: number; end: number; current: { type: RegionType; origin: Origin }; finding: { type: RegionType } };
83
+
84
+ export interface ImportCounts {
85
+ added: number;
86
+ changed: number;
87
+ retired: number;
88
+ unchanged: number;
89
+ /** Findings at a place that a user/LLM removal blocks (D36); not written. */
90
+ suppressed: number;
91
+ }
92
+
93
+ export interface ImportResult {
94
+ /** The new revision, or null when nothing changed. */
95
+ revision: number | null;
96
+ symbols: ImportCounts;
97
+ regions: ImportCounts;
98
+ references: ImportCounts;
99
+ conflicts: ImportConflict[];
100
+ }
101
+
102
+ const isCode = (value: SymbolKind | RegionType) => value === "label" || value === "routine" || (CODE_REGION_TYPES as readonly string[]).includes(value);
103
+
104
+ const counts = (): ImportCounts => ({ added: 0, changed: 0, retired: 0, unchanged: 0, suppressed: 0 });
105
+
106
+ function invalid(message: string): never {
107
+ throw new KnowledgeError("invalid-input", `The analyzer result is not usable: ${message} Nothing was imported.`);
108
+ }
109
+
110
+ /** Sorted, merged coverage ranges. */
111
+ function mergeCoverage(coverage: AddressRange[]): AddressRange[] {
112
+ const sorted = [...coverage].sort((a, b) => a.start - b.start);
113
+ const merged: AddressRange[] = [];
114
+ for (const range of sorted) {
115
+ const last = merged.at(-1);
116
+ if (last !== undefined && range.start <= last.end + 1) last.end = Math.max(last.end, range.end);
117
+ else merged.push({ ...range });
118
+ }
119
+ return merged;
120
+ }
121
+
122
+ /**
123
+ * Checks complete findings before anything is written. Every finding
124
+ * lies in coverage, names are valid and unique, regions do not overlap.
125
+ * Returns the merged coverage.
126
+ */
127
+ export function validateFindings(findings: NormalizedFindings): AddressRange[] {
128
+ if (!(ANALYZERS as readonly string[]).includes(findings.analyzer)) invalid(`the analyzer must be one of ${ANALYZERS.join(", ")}.`);
129
+ checkSpace(findings);
130
+ if (!Array.isArray(findings.coverage) || findings.coverage.length === 0) invalid("the result has no coverage.");
131
+ for (const range of findings.coverage) {
132
+ checkAddress(range.start, "coverage start");
133
+ checkAddress(range.end, "coverage end");
134
+ if (range.end < range.start) invalid(`coverage ${hex(range.start)}-${hex(range.end)} ends before it starts.`);
135
+ }
136
+ const coverage = mergeCoverage(findings.coverage);
137
+ const covering = (start: number, end: number) => coverage.some((range) => range.start <= start && end <= range.end);
138
+
139
+ const addresses = new Set<number>();
140
+ const names = new Set<string>();
141
+ for (const symbol of findings.symbols) {
142
+ checkAddress(symbol.address);
143
+ if (!covering(symbol.address, symbol.address)) invalid(`the symbol at ${hex(symbol.address)} is outside the coverage.`);
144
+ if (!SYMBOL_NAME.test(symbol.name)) invalid(`the symbol name ${JSON.stringify(symbol.name)} is not valid.`);
145
+ if (!(SYMBOL_KINDS as readonly string[]).includes(symbol.kind)) invalid(`the symbol kind ${symbol.kind} is not known.`);
146
+ if (addresses.has(symbol.address)) invalid(`two symbols are at ${hex(symbol.address)}.`);
147
+ if (names.has(symbol.name)) invalid(`the name ${symbol.name} is at two addresses.`);
148
+ addresses.add(symbol.address);
149
+ names.add(symbol.name);
150
+ }
151
+ const regions = [...findings.regions].sort((a, b) => a.start - b.start);
152
+ regions.forEach((region, index) => {
153
+ checkAddress(region.start, "region start");
154
+ checkAddress(region.end, "region end");
155
+ if (region.end < region.start) invalid(`the region ${hex(region.start)}-${hex(region.end)} ends before it starts.`);
156
+ if (!covering(region.start, region.end)) invalid(`the region ${hex(region.start)}-${hex(region.end)} is not inside the coverage.`);
157
+ if (!(REGION_TYPES as readonly string[]).includes(region.type)) invalid(`the region type ${region.type} is not known.`);
158
+ const previous = regions[index - 1];
159
+ if (previous !== undefined && region.start <= previous.end) invalid(`the regions at ${hex(previous.start)} and ${hex(region.start)} overlap.`);
160
+ });
161
+ for (const reference of findings.references) {
162
+ checkAddress(reference.from, "reference source");
163
+ checkAddress(reference.to, "reference target");
164
+ if (!covering(reference.from, reference.from)) invalid(`the reference from ${hex(reference.from)} is outside the coverage.`);
165
+ if (!(REFERENCE_KINDS as readonly string[]).includes(reference.kind)) invalid(`the reference kind ${reference.kind} is not known.`);
166
+ }
167
+ return coverage;
168
+ }
169
+
170
+ function ownSymbols(db: DatabaseSync, analyzer: Analyzer, range: AddressRange, space: Space): SymbolRow[] {
171
+ return db
172
+ .prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_symbols WHERE space = ? AND origin = ? AND address BETWEEN ? AND ? ORDER BY address`)
173
+ .all(space, analyzer, range.start, range.end)
174
+ .map((row) => toSymbol(row as Record<string, unknown>));
175
+ }
176
+
177
+ function ownReferences(db: DatabaseSync, analyzer: Analyzer, range: AddressRange, space: Space): ReferenceRow[] {
178
+ return db
179
+ .prepare(`SELECT ${REFERENCE_COLUMNS} FROM current_references WHERE space = ? AND origin = ? AND from_address BETWEEN ? AND ? ORDER BY from_address`)
180
+ .all(space, analyzer, range.start, range.end)
181
+ .map((row) => toReference(row as Record<string, unknown>));
182
+ }
183
+
184
+ function importSymbols(db: DatabaseSync, change: Change, findings: NormalizedFindings, coverage: AddressRange[], result: ImportResult): void {
185
+ const { analyzer } = findings;
186
+ const space = findings.space ?? DEFAULT_SPACE;
187
+ const wanted = new Map(findings.symbols.map((symbol) => [symbol.address, symbol]));
188
+ const settled = new Set<number>();
189
+ const replaced = new Set<number>();
190
+ // Close first, so a name can move between addresses within one import.
191
+ for (const range of coverage) {
192
+ for (const own of ownSymbols(db, analyzer, range, space)) {
193
+ const finding = wanted.get(own.address);
194
+ if (finding === undefined) {
195
+ if (!findings.authoritative.symbols) continue;
196
+ change.close("symbols", "space = ? AND address = ?", space, own.address);
197
+ result.symbols.retired++;
198
+ } else if (finding.name === own.name && finding.kind === own.kind) {
199
+ settled.add(own.address);
200
+ result.symbols.unchanged++;
201
+ } else {
202
+ change.close("symbols", "space = ? AND address = ?", space, own.address);
203
+ replaced.add(own.address);
204
+ }
205
+ }
206
+ }
207
+ for (const finding of findings.symbols) {
208
+ if (settled.has(finding.address)) continue;
209
+ if (symbolSuppressed(db, finding.address, space)) {
210
+ result.symbols.suppressed++;
211
+ continue;
212
+ }
213
+ let current = symbolAt(db, finding.address, space);
214
+ if (current !== undefined && isCode(finding.kind) && !isCode(current.kind) && YIELDS_TO_EXECUTED_CODE.has(current.origin) && findings.executed?.(finding.address) === true) {
215
+ // A run executed this address: the data symbol of another analyzer gives way (D58).
216
+ change.close("symbols", "space = ? AND address = ?", space, current.address);
217
+ result.symbols.retired++;
218
+ current = undefined;
219
+ }
220
+ if (current !== undefined) {
221
+ // A semantic or other-analyzer row stays; only a code/data disagreement matters.
222
+ if (isCode(current.kind) !== isCode(finding.kind)) {
223
+ result.conflicts.push({ category: "symbol", address: finding.address, reason: "kind", current, finding: { name: finding.name, kind: finding.kind } });
224
+ }
225
+ continue;
226
+ }
227
+ const owner = symbolNamed(db, finding.name, space);
228
+ if (owner?.origin === analyzer) {
229
+ // The analyzer moved its own name: its newer finding replaces its own older row.
230
+ change.close("symbols", "space = ? AND address = ?", space, owner.address);
231
+ result.symbols.retired++;
232
+ } else if (owner !== undefined) {
233
+ result.conflicts.push({ category: "symbol", address: finding.address, reason: "name-taken", current: owner, finding: { name: finding.name, kind: finding.kind } });
234
+ if (replaced.has(finding.address)) result.symbols.retired++;
235
+ continue;
236
+ }
237
+ change.insertSymbol({ space, address: finding.address, name: finding.name, kind: finding.kind, origin: analyzer });
238
+ if (replaced.has(finding.address)) result.symbols.changed++;
239
+ else result.symbols.added++;
240
+ }
241
+ }
242
+
243
+ /** Analyzers whose regions a heuristic finding replaces where it lies (D44): user/LLM > heuristic > DXA/Ghidra. */
244
+ const YIELDS_TO_HEURISTIC = new Set<Origin>(["dxa", "ghidra"]);
245
+ /** Analyzers whose data gives way to a code finding over an executed instruction start (D58). */
246
+ const YIELDS_TO_EXECUTED_CODE = new Set<Origin>(["dxa", "ghidra"]);
247
+
248
+ /** True when an address in the range is an executed instruction start. */
249
+ function holdsExecuted(findings: NormalizedFindings, start: number, end: number): boolean {
250
+ if (findings.executed === undefined) return false;
251
+ for (let address = start; address <= end; address++) if (findings.executed(address)) return true;
252
+ return false;
253
+ }
254
+
255
+ function importRegions(db: DatabaseSync, change: Change, findings: NormalizedFindings, coverage: AddressRange[], result: ImportResult): void {
256
+ const { analyzer } = findings;
257
+ const space = findings.space ?? DEFAULT_SPACE;
258
+ if (analyzer === "heuristic") {
259
+ // An idiom is specific evidence about its bytes: the DXA or Ghidra region there gives way, its parts outside stay.
260
+ for (const region of findings.regions) {
261
+ for (const row of regionsOverlapping(db, region.start, region.end, space).filter((candidate) => YIELDS_TO_HEURISTIC.has(candidate.origin))) {
262
+ change.close("regions", "space = ? AND start_address = ? AND end_address = ? AND origin = ?", space, row.start, row.end, row.origin);
263
+ for (const piece of subtract(row, [region])) change.insertRegion({ space, ...piece, type: row.type, ...(row.element === undefined ? {} : { element: row.element }), origin: row.origin });
264
+ }
265
+ }
266
+ }
267
+ // Executed code is evidence about its bytes (D58): another analyzer's data region under a code finding gives way where a run executed an instruction start.
268
+ for (const region of findings.regions.filter((candidate) => isCode(candidate.type))) {
269
+ for (const row of regionsOverlapping(db, region.start, region.end, space)) {
270
+ if (row.origin === analyzer || !YIELDS_TO_EXECUTED_CODE.has(row.origin) || isCode(row.type) || row.type === "dual-use") continue;
271
+ if (!holdsExecuted(findings, Math.max(row.start, region.start), Math.min(row.end, region.end))) continue;
272
+ change.close("regions", "space = ? AND start_address = ? AND end_address = ? AND origin = ?", space, row.start, row.end, row.origin);
273
+ for (const piece of subtract(row, [region])) change.insertRegion({ space, ...piece, type: row.type, ...(row.element === undefined ? {} : { element: row.element }), origin: row.origin });
274
+ result.regions.retired++;
275
+ }
276
+ }
277
+ const conflictWith = (row: RegionRow, region: { start: number; end: number; type: RegionType }) => {
278
+ // dual-use bytes are code at one time and data at another: neither finding contradicts them (D42).
279
+ if (isCode(row.type) === isCode(region.type) || row.type === "dual-use") return;
280
+ result.conflicts.push({
281
+ category: "region",
282
+ start: Math.max(row.start, region.start),
283
+ end: Math.min(row.end, region.end),
284
+ current: { type: row.type, origin: row.origin },
285
+ finding: { type: region.type },
286
+ });
287
+ };
288
+
289
+ if (!findings.authoritative.regions) {
290
+ // Retire nothing the snapshot leaves out. Fill addresses that no region covers yet, and
291
+ // give the analyzer's own regions under a finding the type that the analyzer finds now.
292
+ for (const region of findings.regions) {
293
+ const suppressed = suppressionsOverlapping(db, "region", region.start, region.end, space);
294
+ if (suppressed.length > 0) result.regions.suppressed++;
295
+ const current = regionsOverlapping(db, region.start, region.end, space);
296
+ const protectedRows = current.filter((row) => row.origin !== analyzer);
297
+ for (const row of protectedRows) conflictWith(row, region);
298
+ const own = current.filter((row) => row.origin === analyzer);
299
+ const kept = own.filter((row) => row.type === region.type);
300
+ result.regions.unchanged += kept.length;
301
+ for (const row of own.filter((candidate) => candidate.type !== region.type)) {
302
+ // Close the row and keep its parts outside the finding.
303
+ change.close("regions", "space = ? AND start_address = ? AND end_address = ? AND origin = ?", space, row.start, row.end, analyzer);
304
+ if (row.start < region.start) change.insertRegion({ space, start: row.start, end: region.start - 1, type: row.type, origin: analyzer });
305
+ if (row.end > region.end) change.insertRegion({ space, start: region.end + 1, end: row.end, type: row.type, origin: analyzer });
306
+ result.regions.changed++;
307
+ }
308
+ const empty = subtract(region, [...current, ...suppressed]);
309
+ for (const piece of subtract(region, [...protectedRows, ...kept, ...suppressed])) {
310
+ change.insertRegion({ space, ...piece, type: region.type, origin: analyzer });
311
+ }
312
+ result.regions.added += empty.length;
313
+ }
314
+ return;
315
+ }
316
+
317
+ for (const range of coverage) {
318
+ const current = regionsOverlapping(db, range.start, range.end, space);
319
+ const protectedRows = current.filter((row) => row.origin !== analyzer);
320
+ const own = current.filter((row) => row.origin === analyzer);
321
+ // What the analyzer may own here: its regions minus protected rows.
322
+ const desired = new Map<string, { start: number; end: number; type: RegionType }>();
323
+ for (const region of findings.regions.filter((candidate) => candidate.start >= range.start && candidate.end <= range.end)) {
324
+ for (const row of protectedRows) if (row.start <= region.end && row.end >= region.start) conflictWith(row, region);
325
+ const suppressed = suppressionsOverlapping(db, "region", region.start, region.end, space);
326
+ if (suppressed.length > 0) result.regions.suppressed++;
327
+ for (const piece of subtract(region, [...protectedRows, ...suppressed])) desired.set(`${piece.start}-${piece.end}`, { ...piece, type: region.type });
328
+ }
329
+ const retiredRanges = new Set<string>();
330
+ for (const row of own) {
331
+ const inside = { start: Math.max(row.start, range.start), end: Math.min(row.end, range.end) };
332
+ const key = `${inside.start}-${inside.end}`;
333
+ if (desired.get(key)?.type === row.type) {
334
+ desired.delete(key);
335
+ result.regions.unchanged++;
336
+ continue;
337
+ }
338
+ // Close the row and keep its parts outside the coverage.
339
+ change.close("regions", "space = ? AND start_address = ? AND end_address = ? AND origin = ?", space, row.start, row.end, analyzer);
340
+ if (row.start < range.start) change.insertRegion({ space, start: row.start, end: range.start - 1, type: row.type, origin: analyzer });
341
+ if (row.end > range.end) change.insertRegion({ space, start: range.end + 1, end: row.end, type: row.type, origin: analyzer });
342
+ retiredRanges.add(key);
343
+ }
344
+ for (const [key, piece] of desired) {
345
+ change.insertRegion({ space, ...piece, origin: analyzer });
346
+ if (retiredRanges.delete(key)) result.regions.changed++;
347
+ else result.regions.added++;
348
+ }
349
+ result.regions.retired += retiredRanges.size;
350
+ }
351
+ }
352
+
353
+ function importReferences(db: DatabaseSync, change: Change, findings: NormalizedFindings, coverage: AddressRange[], result: ImportResult): void {
354
+ const { analyzer } = findings;
355
+ const space = findings.space ?? DEFAULT_SPACE;
356
+ const key = (reference: { from: number; to: number; kind: ReferenceKind }) => `${reference.from}|${reference.to}|${reference.kind}`;
357
+ const wanted = new Map(findings.references.map((reference) => [key(reference), reference]));
358
+ for (const range of coverage) {
359
+ for (const own of ownReferences(db, analyzer, range, space)) {
360
+ if (wanted.has(key(own))) continue;
361
+ if (!findings.authoritative.references) continue;
362
+ change.close('"references"', "space = ? AND from_address = ? AND to_address = ? AND kind = ? AND origin = ?", space, own.from, own.to, own.kind, analyzer);
363
+ result.references.retired++;
364
+ }
365
+ }
366
+ const existing = db.prepare("SELECT origin FROM current_references WHERE space = ? AND from_address = ? AND to_address = ? AND kind = ?");
367
+ for (const reference of wanted.values()) {
368
+ const rows = existing.all(space, reference.from, reference.to, reference.kind) as Array<{ origin: Origin }>;
369
+ if (rows.some((row) => row.origin === analyzer)) result.references.unchanged++;
370
+ else if (rows.length === 0 && referenceSuppressed(db, reference, space)) result.references.suppressed++;
371
+ // The same fact from the user, the LLM or another analyzer already holds.
372
+ else if (rows.length === 0) {
373
+ change.insertReference({ space, ...reference, origin: analyzer });
374
+ result.references.added++;
375
+ }
376
+ }
377
+ }
378
+
379
+ /** Imports one analyzer result as one revision. Nothing is written when validation fails. */
380
+ export function importFindings(db: DatabaseSync, findings: NormalizedFindings, context: ImportContext = {}): ImportResult {
381
+ const coverage = validateFindings(findings);
382
+ return transaction(db, () => {
383
+ const change = new Change(db, { origin: findings.analyzer, ...context }, `import-${findings.analyzer}`);
384
+ const result: ImportResult = { revision: null, symbols: counts(), regions: counts(), references: counts(), conflicts: [] };
385
+ importSymbols(db, change, findings, coverage, result);
386
+ importRegions(db, change, findings, coverage, result);
387
+ importReferences(db, change, findings, coverage, result);
388
+ result.revision = change.revision;
389
+ return result;
390
+ });
391
+ }