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

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 (162) 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 +192 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +613 -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 +133 -0
  26. package/skills/c64-source/scripts/generate.ts +503 -0
  27. package/skills/c64-source/scripts/loader.ts +589 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +673 -0
  30. package/skills/c64-static-analysis/SKILL.md +120 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +293 -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-static-analysis/scripts/pointers.ts +108 -0
  37. package/skills/c64-testing/SKILL.md +103 -0
  38. package/skills/c64-testing/references/scenario-format.md +116 -0
  39. package/skills/c64-testing/scripts/checklist.ts +51 -0
  40. package/skills/c64-testing/scripts/run.ts +189 -0
  41. package/skills/c64-testing/scripts/scenario.ts +493 -0
  42. package/skills/c64-testing/scripts/test.ts +65 -0
  43. package/skills/c64-unpacker/SKILL.md +135 -0
  44. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  45. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  46. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  47. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  48. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  49. package/src/c64.ts +411 -0
  50. package/src/cli/ap-sdk.ts +44 -0
  51. package/src/cli/cleanup.ts +194 -0
  52. package/src/cli/main.ts +125 -0
  53. package/src/cli/status.ts +61 -0
  54. package/src/host/main.ts +134 -0
  55. package/src/host/server.ts +392 -0
  56. package/src/host/tools/c1541.ts +284 -0
  57. package/src/host/tools/index.ts +35 -0
  58. package/src/host/tools/petcat.ts +77 -0
  59. package/src/host/tools/status.ts +47 -0
  60. package/src/host/tools/version.ts +41 -0
  61. package/src/host/vice/adapter.ts +804 -0
  62. package/src/host/vice/binary-monitor.ts +389 -0
  63. package/src/host/vice/chips.ts +118 -0
  64. package/src/host/vice/connect.ts +34 -0
  65. package/src/host/vice/process.ts +413 -0
  66. package/src/host/vice/screen.ts +184 -0
  67. package/src/host/vice/session.ts +1426 -0
  68. package/src/host/vice/text-monitor.ts +223 -0
  69. package/src/host-client/autostart.ts +55 -0
  70. package/src/host-client/connect.ts +241 -0
  71. package/src/host-client/coverage.ts +63 -0
  72. package/src/host-client/image.ts +63 -0
  73. package/src/host-client/steps.ts +170 -0
  74. package/src/host-client/tools.ts +89 -0
  75. package/src/host-client/transfer.ts +126 -0
  76. package/src/host-client/vice-session.ts +316 -0
  77. package/src/knowledge/database.ts +132 -0
  78. package/src/knowledge/expression.ts +209 -0
  79. package/src/knowledge/history.ts +169 -0
  80. package/src/knowledge/import.ts +392 -0
  81. package/src/knowledge/read.ts +392 -0
  82. package/src/knowledge/schema.ts +250 -0
  83. package/src/knowledge/write.ts +837 -0
  84. package/src/mcp/main.ts +134 -0
  85. package/src/mcp/output.ts +32 -0
  86. package/src/mcp/server.ts +316 -0
  87. package/src/mcp/tools/debug.ts +327 -0
  88. package/src/mcp/tools/execution.ts +117 -0
  89. package/src/mcp/tools/input.ts +72 -0
  90. package/src/mcp/tools/machine.ts +100 -0
  91. package/src/mcp/tools/media.ts +129 -0
  92. package/src/mcp/tools/memory.ts +250 -0
  93. package/src/mcp/tools/video.ts +271 -0
  94. package/src/native/acme.ts +116 -0
  95. package/src/native/discover.ts +179 -0
  96. package/src/native/dxa.ts +166 -0
  97. package/src/native/ghidra/analyze.ts +86 -0
  98. package/src/native/ghidra/index.ts +173 -0
  99. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  100. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  101. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  102. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  103. package/src/native/ghidra/scripts/C64Export.java +274 -0
  104. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  105. package/src/native/local.ts +34 -0
  106. package/src/native/processes.ts +350 -0
  107. package/src/native/run.ts +172 -0
  108. package/src/native/staging.ts +83 -0
  109. package/src/native/status.ts +113 -0
  110. package/src/native/types.ts +257 -0
  111. package/src/native/watchdog.ts +78 -0
  112. package/src/package.ts +21 -0
  113. package/src/platform.ts +441 -0
  114. package/src/project.ts +39 -0
  115. package/src/protocol/framing.ts +132 -0
  116. package/src/protocol/messages.ts +259 -0
  117. package/src/protocol/tools.ts +266 -0
  118. package/src/protocol/vice.ts +1348 -0
  119. package/src/script.ts +60 -0
  120. package/src/time.ts +15 -0
  121. package/src/trace.ts +194 -0
  122. package/THIRD-PARTY-NOTICES.md +0 -26
  123. package/bin/cli.mjs +0 -283
  124. package/skills/acme-build/SKILL.md +0 -260
  125. package/skills/acme-build/scripts/acme.mjs +0 -341
  126. package/skills/acme-build/template.a +0 -53
  127. package/skills/c64-disk-access/SKILL.md +0 -159
  128. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  129. package/skills/c64-memory-mapping/SKILL.md +0 -631
  130. package/skills/c64-memory-mapping/memmap.json +0 -8800
  131. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  132. package/skills/c64-petcat/SKILL.md +0 -87
  133. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  134. package/skills/c64-program-recon/SKILL.md +0 -706
  135. package/skills/c64-program-recon/references/control-flow.md +0 -180
  136. package/skills/c64-program-recon/references/graphics.md +0 -79
  137. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  138. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  139. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  140. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  141. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  142. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  143. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  144. package/skills/c64-provenance-diff/SKILL.md +0 -292
  145. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  146. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  147. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  148. package/skills/c64-ram-capture/SKILL.md +0 -474
  149. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  150. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  151. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  152. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  153. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  154. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  155. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  156. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  157. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  158. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  159. package/skills/c64-ram-capture/transients/README.md +0 -137
  160. package/skills/routine-queue-walker/SKILL.md +0 -394
  161. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  162. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -0,0 +1,169 @@
1
+ // Knowledge history: what used to be known, which revision changed
2
+ // it, who changed it and why. History rows are never deleted.
3
+
4
+ import type { DatabaseSync } from "node:sqlite";
5
+
6
+ import { KnowledgeError } from "./database.ts";
7
+ import {
8
+ COMMENT_COLUMNS,
9
+ MODULE_COLUMNS,
10
+ OPERAND_COLUMNS,
11
+ REFERENCE_COLUMNS,
12
+ REGION_COLUMNS,
13
+ SUPPRESSION_COLUMNS,
14
+ SYMBOL_COLUMNS,
15
+ toComment,
16
+ toModule,
17
+ toOperand,
18
+ toReference,
19
+ toRegion,
20
+ toSuppression,
21
+ toSymbol,
22
+ type CommentRow,
23
+ type ModuleRow,
24
+ type OperandRow,
25
+ type ReferenceRow,
26
+ type RegionRow,
27
+ type SuppressionRow,
28
+ type SymbolRow,
29
+ } from "./read.ts";
30
+ import { DEFAULT_SPACE, type EvidenceKind, type Origin, type Space } from "./schema.ts";
31
+ import type { Evidence } from "./write.ts";
32
+
33
+ export interface Revision {
34
+ id: number;
35
+ createdAt: string;
36
+ origin: Origin;
37
+ operation: string;
38
+ description?: string;
39
+ inputHash?: string;
40
+ toolVersion?: string;
41
+ /** What the change rests on (D42). */
42
+ evidence?: Evidence[];
43
+ }
44
+
45
+ /** One knowledge row with its validity: from its first revision until the revision that closed it. */
46
+ export type HistoryEntry =
47
+ | { entity: "symbol"; row: SymbolRow; from: number; to?: number }
48
+ | { entity: "region"; row: RegionRow; from: number; to?: number }
49
+ | { entity: "comment"; row: CommentRow; from: number; to?: number }
50
+ | { entity: "reference"; row: ReferenceRow; from: number; to?: number }
51
+ | { entity: "suppression"; row: SuppressionRow; from: number; to?: number }
52
+ | { entity: "module"; row: ModuleRow; from: number; to?: number }
53
+ | { entity: "operand"; row: OperandRow; from: number; to?: number };
54
+
55
+ type Raw = Record<string, unknown>;
56
+
57
+ function toRevision(row: Raw): Revision {
58
+ const revision: Revision = {
59
+ id: row.id as number,
60
+ createdAt: row.created_at as string,
61
+ origin: row.origin as Origin,
62
+ operation: row.operation as string,
63
+ };
64
+ if (row.description !== null) revision.description = row.description as string;
65
+ if (row.input_hash !== null) revision.inputHash = row.input_hash as string;
66
+ if (row.tool_version !== null) revision.toolVersion = row.tool_version as string;
67
+ return revision;
68
+ }
69
+
70
+ function entry<E extends HistoryEntry["entity"]>(entity: E, row: unknown, raw: Raw): HistoryEntry {
71
+ const result = { entity, row, from: raw.valid_from_revision as number } as HistoryEntry;
72
+ if (raw.valid_to_revision !== null) result.to = raw.valid_to_revision as number;
73
+ return result;
74
+ }
75
+
76
+ function bySequence(a: HistoryEntry, b: HistoryEntry): number {
77
+ return a.from - b.from || (a.to ?? Infinity) - (b.to ?? Infinity);
78
+ }
79
+
80
+ /** Every row, current or closed, that concerns one address of a space, oldest first. */
81
+ export function historyAt(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): HistoryEntry[] {
82
+ if (db === undefined) return [];
83
+ const validity = ", valid_to_revision";
84
+ const entries: HistoryEntry[] = [];
85
+ for (const raw of db.prepare(`SELECT ${SYMBOL_COLUMNS}${validity} FROM symbols WHERE space = ? AND address = ?`).all(space, address) as Raw[]) entries.push(entry("symbol", toSymbol(raw), raw));
86
+ for (const raw of db.prepare(`SELECT ${REGION_COLUMNS}${validity} FROM regions WHERE space = ? AND start_address <= ? AND end_address >= ?`).all(space, address, address) as Raw[]) {
87
+ entries.push(entry("region", toRegion(raw), raw));
88
+ }
89
+ for (const raw of db.prepare(`SELECT ${COMMENT_COLUMNS}${validity} FROM comments WHERE space = ? AND address = ?`).all(space, address) as Raw[]) entries.push(entry("comment", toComment(raw), raw));
90
+ for (const raw of db.prepare(`SELECT ${REFERENCE_COLUMNS}${validity} FROM "references" WHERE space = ? AND (from_address = ? OR to_address = ?)`).all(space, address, address) as Raw[]) {
91
+ entries.push(entry("reference", toReference(raw), raw));
92
+ }
93
+ for (const raw of db
94
+ .prepare(`SELECT ${SUPPRESSION_COLUMNS}${validity} FROM suppressions WHERE space = ? AND ((start_address <= ? AND end_address >= ?) OR to_address = ?)`)
95
+ .all(space, address, address, address) as Raw[]) {
96
+ entries.push(entry("suppression", toSuppression(raw), raw));
97
+ }
98
+ for (const raw of db.prepare(`SELECT ${MODULE_COLUMNS}${validity} FROM modules WHERE space = ? AND start_address = ?`).all(space, address) as Raw[]) entries.push(entry("module", toModule(raw), raw));
99
+ for (const raw of db.prepare(`SELECT ${OPERAND_COLUMNS}${validity} FROM operands WHERE space = ? AND address = ?`).all(space, address) as Raw[]) entries.push(entry("operand", toOperand(raw), raw));
100
+ return entries.sort(bySequence);
101
+ }
102
+
103
+ /** Every symbol row of a space that ever had this name, oldest first: a name can move between addresses. */
104
+ export function historyNamed(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): HistoryEntry[] {
105
+ if (db === undefined) return [];
106
+ return (db.prepare(`SELECT ${SYMBOL_COLUMNS}, valid_to_revision FROM symbols WHERE space = ? AND name = ?`).all(space, name) as Raw[])
107
+ .map((raw) => entry("symbol", toSymbol(raw), raw))
108
+ .sort(bySequence);
109
+ }
110
+
111
+ /** Why a revision was written and what it rests on: its description and its evidence, when it has them. */
112
+ export function revisionNote(db: DatabaseSync | undefined, id: number): { reason?: string; evidence?: Evidence[] } {
113
+ const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
114
+ if (db === undefined || raw === undefined) return {};
115
+ const { description, evidence } = withEvidence(db, toRevision(raw));
116
+ return { ...(description === undefined ? {} : { reason: description }), ...(evidence === undefined ? {} : { evidence }) };
117
+ }
118
+
119
+ /** The revision with its evidence, when it has any. */
120
+ function withEvidence(db: DatabaseSync, revision: Revision): Revision {
121
+ const rows = db.prepare("SELECT kind, ref, sha256 FROM evidence WHERE revision = ? ORDER BY id").all(revision.id) as Raw[];
122
+ if (rows.length === 0) return revision;
123
+ const evidence = rows.map((row): Evidence => ({ kind: row.kind as EvidenceKind, ref: row.ref as string, ...(row.sha256 === null ? {} : { sha256: row.sha256 as string }) }));
124
+ return { ...revision, evidence };
125
+ }
126
+
127
+ /** Revisions, newest first; with `before`, only revisions older than it. */
128
+ export function revisions(db: DatabaseSync | undefined, options: { limit?: number; before?: number } = {}): Revision[] {
129
+ if (db === undefined) return [];
130
+ return (
131
+ db
132
+ .prepare("SELECT * FROM revisions WHERE id < ? ORDER BY id DESC LIMIT ?")
133
+ .all(options.before ?? Number.MAX_SAFE_INTEGER, options.limit ?? 50) as Raw[]
134
+ ).map((row) => withEvidence(db, toRevision(row)));
135
+ }
136
+
137
+ export interface RevisionChanges {
138
+ revision: Revision;
139
+ /** Rows this revision made current. */
140
+ added: HistoryEntry[];
141
+ /** Rows this revision closed. */
142
+ closed: HistoryEntry[];
143
+ }
144
+
145
+ /** One revision and exactly what it changed. */
146
+ export function revision(db: DatabaseSync | undefined, id: number): RevisionChanges {
147
+ const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
148
+ if (db === undefined || raw === undefined) throw new KnowledgeError("not-found", `There is no revision ${id}.`);
149
+ const tables = [
150
+ ["symbol", "symbols", SYMBOL_COLUMNS, toSymbol],
151
+ ["region", "regions", REGION_COLUMNS, toRegion],
152
+ ["comment", "comments", COMMENT_COLUMNS, toComment],
153
+ ["reference", '"references"', REFERENCE_COLUMNS, toReference],
154
+ ["suppression", "suppressions", SUPPRESSION_COLUMNS, toSuppression],
155
+ ["module", "modules", MODULE_COLUMNS, toModule],
156
+ ["operand", "operands", OPERAND_COLUMNS, toOperand],
157
+ ] as const;
158
+ const added: HistoryEntry[] = [];
159
+ const closed: HistoryEntry[] = [];
160
+ for (const [entity, table, columns, map] of tables) {
161
+ for (const row of db.prepare(`SELECT ${columns}, valid_to_revision FROM ${table} WHERE valid_from_revision = ?`).all(id) as Raw[]) {
162
+ added.push(entry(entity, map(row), row));
163
+ }
164
+ for (const row of db.prepare(`SELECT ${columns}, valid_to_revision FROM ${table} WHERE valid_to_revision = ?`).all(id) as Raw[]) {
165
+ closed.push(entry(entity, map(row), row));
166
+ }
167
+ }
168
+ return { revision: withEvidence(db, toRevision(raw)), added, closed };
169
+ }
@@ -0,0 +1,392 @@
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, POINTER_KIND, 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
+ if (reference.kind === POINTER_KIND) invalid(`only an operand expression writes a pointer reference (D65), not an analyzer.`);
167
+ }
168
+ return coverage;
169
+ }
170
+
171
+ function ownSymbols(db: DatabaseSync, analyzer: Analyzer, range: AddressRange, space: Space): SymbolRow[] {
172
+ return db
173
+ .prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_symbols WHERE space = ? AND origin = ? AND address BETWEEN ? AND ? ORDER BY address`)
174
+ .all(space, analyzer, range.start, range.end)
175
+ .map((row) => toSymbol(row as Record<string, unknown>));
176
+ }
177
+
178
+ function ownReferences(db: DatabaseSync, analyzer: Analyzer, range: AddressRange, space: Space): ReferenceRow[] {
179
+ return db
180
+ .prepare(`SELECT ${REFERENCE_COLUMNS} FROM current_references WHERE space = ? AND origin = ? AND from_address BETWEEN ? AND ? ORDER BY from_address`)
181
+ .all(space, analyzer, range.start, range.end)
182
+ .map((row) => toReference(row as Record<string, unknown>));
183
+ }
184
+
185
+ function importSymbols(db: DatabaseSync, change: Change, findings: NormalizedFindings, coverage: AddressRange[], result: ImportResult): void {
186
+ const { analyzer } = findings;
187
+ const space = findings.space ?? DEFAULT_SPACE;
188
+ const wanted = new Map(findings.symbols.map((symbol) => [symbol.address, symbol]));
189
+ const settled = new Set<number>();
190
+ const replaced = new Set<number>();
191
+ // Close first, so a name can move between addresses within one import.
192
+ for (const range of coverage) {
193
+ for (const own of ownSymbols(db, analyzer, range, space)) {
194
+ const finding = wanted.get(own.address);
195
+ if (finding === undefined) {
196
+ if (!findings.authoritative.symbols) continue;
197
+ change.close("symbols", "space = ? AND address = ?", space, own.address);
198
+ result.symbols.retired++;
199
+ } else if (finding.name === own.name && finding.kind === own.kind) {
200
+ settled.add(own.address);
201
+ result.symbols.unchanged++;
202
+ } else {
203
+ change.close("symbols", "space = ? AND address = ?", space, own.address);
204
+ replaced.add(own.address);
205
+ }
206
+ }
207
+ }
208
+ for (const finding of findings.symbols) {
209
+ if (settled.has(finding.address)) continue;
210
+ if (symbolSuppressed(db, finding.address, space)) {
211
+ result.symbols.suppressed++;
212
+ continue;
213
+ }
214
+ let current = symbolAt(db, finding.address, space);
215
+ if (current !== undefined && isCode(finding.kind) && !isCode(current.kind) && YIELDS_TO_EXECUTED_CODE.has(current.origin) && findings.executed?.(finding.address) === true) {
216
+ // A run executed this address: the data symbol of another analyzer gives way (D58).
217
+ change.close("symbols", "space = ? AND address = ?", space, current.address);
218
+ result.symbols.retired++;
219
+ current = undefined;
220
+ }
221
+ if (current !== undefined) {
222
+ // A semantic or other-analyzer row stays; only a code/data disagreement matters.
223
+ if (isCode(current.kind) !== isCode(finding.kind)) {
224
+ result.conflicts.push({ category: "symbol", address: finding.address, reason: "kind", current, finding: { name: finding.name, kind: finding.kind } });
225
+ }
226
+ continue;
227
+ }
228
+ const owner = symbolNamed(db, finding.name, space);
229
+ if (owner?.origin === analyzer) {
230
+ // The analyzer moved its own name: its newer finding replaces its own older row.
231
+ change.close("symbols", "space = ? AND address = ?", space, owner.address);
232
+ result.symbols.retired++;
233
+ } else if (owner !== undefined) {
234
+ result.conflicts.push({ category: "symbol", address: finding.address, reason: "name-taken", current: owner, finding: { name: finding.name, kind: finding.kind } });
235
+ if (replaced.has(finding.address)) result.symbols.retired++;
236
+ continue;
237
+ }
238
+ change.insertSymbol({ space, address: finding.address, name: finding.name, kind: finding.kind, origin: analyzer });
239
+ if (replaced.has(finding.address)) result.symbols.changed++;
240
+ else result.symbols.added++;
241
+ }
242
+ }
243
+
244
+ /** Analyzers whose regions a heuristic finding replaces where it lies (D44): user/LLM > heuristic > DXA/Ghidra. */
245
+ const YIELDS_TO_HEURISTIC = new Set<Origin>(["dxa", "ghidra"]);
246
+ /** Analyzers whose data gives way to a code finding over an executed instruction start (D58). */
247
+ const YIELDS_TO_EXECUTED_CODE = new Set<Origin>(["dxa", "ghidra"]);
248
+
249
+ /** True when an address in the range is an executed instruction start. */
250
+ function holdsExecuted(findings: NormalizedFindings, start: number, end: number): boolean {
251
+ if (findings.executed === undefined) return false;
252
+ for (let address = start; address <= end; address++) if (findings.executed(address)) return true;
253
+ return false;
254
+ }
255
+
256
+ function importRegions(db: DatabaseSync, change: Change, findings: NormalizedFindings, coverage: AddressRange[], result: ImportResult): void {
257
+ const { analyzer } = findings;
258
+ const space = findings.space ?? DEFAULT_SPACE;
259
+ if (analyzer === "heuristic") {
260
+ // An idiom is specific evidence about its bytes: the DXA or Ghidra region there gives way, its parts outside stay.
261
+ for (const region of findings.regions) {
262
+ for (const row of regionsOverlapping(db, region.start, region.end, space).filter((candidate) => YIELDS_TO_HEURISTIC.has(candidate.origin))) {
263
+ change.close("regions", "space = ? AND start_address = ? AND end_address = ? AND origin = ?", space, row.start, row.end, row.origin);
264
+ for (const piece of subtract(row, [region])) change.insertRegion({ space, ...piece, type: row.type, ...(row.element === undefined ? {} : { element: row.element }), origin: row.origin });
265
+ }
266
+ }
267
+ }
268
+ // 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.
269
+ for (const region of findings.regions.filter((candidate) => isCode(candidate.type))) {
270
+ for (const row of regionsOverlapping(db, region.start, region.end, space)) {
271
+ if (row.origin === analyzer || !YIELDS_TO_EXECUTED_CODE.has(row.origin) || isCode(row.type) || row.type === "dual-use") continue;
272
+ if (!holdsExecuted(findings, Math.max(row.start, region.start), Math.min(row.end, region.end))) continue;
273
+ change.close("regions", "space = ? AND start_address = ? AND end_address = ? AND origin = ?", space, row.start, row.end, row.origin);
274
+ for (const piece of subtract(row, [region])) change.insertRegion({ space, ...piece, type: row.type, ...(row.element === undefined ? {} : { element: row.element }), origin: row.origin });
275
+ result.regions.retired++;
276
+ }
277
+ }
278
+ const conflictWith = (row: RegionRow, region: { start: number; end: number; type: RegionType }) => {
279
+ // dual-use bytes are code at one time and data at another: neither finding contradicts them (D42).
280
+ if (isCode(row.type) === isCode(region.type) || row.type === "dual-use") return;
281
+ result.conflicts.push({
282
+ category: "region",
283
+ start: Math.max(row.start, region.start),
284
+ end: Math.min(row.end, region.end),
285
+ current: { type: row.type, origin: row.origin },
286
+ finding: { type: region.type },
287
+ });
288
+ };
289
+
290
+ if (!findings.authoritative.regions) {
291
+ // Retire nothing the snapshot leaves out. Fill addresses that no region covers yet, and
292
+ // give the analyzer's own regions under a finding the type that the analyzer finds now.
293
+ for (const region of findings.regions) {
294
+ const suppressed = suppressionsOverlapping(db, "region", region.start, region.end, space);
295
+ if (suppressed.length > 0) result.regions.suppressed++;
296
+ const current = regionsOverlapping(db, region.start, region.end, space);
297
+ const protectedRows = current.filter((row) => row.origin !== analyzer);
298
+ for (const row of protectedRows) conflictWith(row, region);
299
+ const own = current.filter((row) => row.origin === analyzer);
300
+ const kept = own.filter((row) => row.type === region.type);
301
+ result.regions.unchanged += kept.length;
302
+ for (const row of own.filter((candidate) => candidate.type !== region.type)) {
303
+ // Close the row and keep its parts outside the finding.
304
+ change.close("regions", "space = ? AND start_address = ? AND end_address = ? AND origin = ?", space, row.start, row.end, analyzer);
305
+ if (row.start < region.start) change.insertRegion({ space, start: row.start, end: region.start - 1, type: row.type, origin: analyzer });
306
+ if (row.end > region.end) change.insertRegion({ space, start: region.end + 1, end: row.end, type: row.type, origin: analyzer });
307
+ result.regions.changed++;
308
+ }
309
+ const empty = subtract(region, [...current, ...suppressed]);
310
+ for (const piece of subtract(region, [...protectedRows, ...kept, ...suppressed])) {
311
+ change.insertRegion({ space, ...piece, type: region.type, origin: analyzer });
312
+ }
313
+ result.regions.added += empty.length;
314
+ }
315
+ return;
316
+ }
317
+
318
+ for (const range of coverage) {
319
+ const current = regionsOverlapping(db, range.start, range.end, space);
320
+ const protectedRows = current.filter((row) => row.origin !== analyzer);
321
+ const own = current.filter((row) => row.origin === analyzer);
322
+ // What the analyzer may own here: its regions minus protected rows.
323
+ const desired = new Map<string, { start: number; end: number; type: RegionType }>();
324
+ for (const region of findings.regions.filter((candidate) => candidate.start >= range.start && candidate.end <= range.end)) {
325
+ for (const row of protectedRows) if (row.start <= region.end && row.end >= region.start) conflictWith(row, region);
326
+ const suppressed = suppressionsOverlapping(db, "region", region.start, region.end, space);
327
+ if (suppressed.length > 0) result.regions.suppressed++;
328
+ for (const piece of subtract(region, [...protectedRows, ...suppressed])) desired.set(`${piece.start}-${piece.end}`, { ...piece, type: region.type });
329
+ }
330
+ const retiredRanges = new Set<string>();
331
+ for (const row of own) {
332
+ const inside = { start: Math.max(row.start, range.start), end: Math.min(row.end, range.end) };
333
+ const key = `${inside.start}-${inside.end}`;
334
+ if (desired.get(key)?.type === row.type) {
335
+ desired.delete(key);
336
+ result.regions.unchanged++;
337
+ continue;
338
+ }
339
+ // Close the row and keep its parts outside the coverage.
340
+ change.close("regions", "space = ? AND start_address = ? AND end_address = ? AND origin = ?", space, row.start, row.end, analyzer);
341
+ if (row.start < range.start) change.insertRegion({ space, start: row.start, end: range.start - 1, type: row.type, origin: analyzer });
342
+ if (row.end > range.end) change.insertRegion({ space, start: range.end + 1, end: row.end, type: row.type, origin: analyzer });
343
+ retiredRanges.add(key);
344
+ }
345
+ for (const [key, piece] of desired) {
346
+ change.insertRegion({ space, ...piece, origin: analyzer });
347
+ if (retiredRanges.delete(key)) result.regions.changed++;
348
+ else result.regions.added++;
349
+ }
350
+ result.regions.retired += retiredRanges.size;
351
+ }
352
+ }
353
+
354
+ function importReferences(db: DatabaseSync, change: Change, findings: NormalizedFindings, coverage: AddressRange[], result: ImportResult): void {
355
+ const { analyzer } = findings;
356
+ const space = findings.space ?? DEFAULT_SPACE;
357
+ const key = (reference: { from: number; to: number; kind: ReferenceKind }) => `${reference.from}|${reference.to}|${reference.kind}`;
358
+ const wanted = new Map(findings.references.map((reference) => [key(reference), reference]));
359
+ for (const range of coverage) {
360
+ for (const own of ownReferences(db, analyzer, range, space)) {
361
+ if (wanted.has(key(own))) continue;
362
+ if (!findings.authoritative.references) continue;
363
+ change.close('"references"', "space = ? AND from_address = ? AND to_address = ? AND kind = ? AND origin = ?", space, own.from, own.to, own.kind, analyzer);
364
+ result.references.retired++;
365
+ }
366
+ }
367
+ const existing = db.prepare("SELECT origin FROM current_references WHERE space = ? AND from_address = ? AND to_address = ? AND kind = ?");
368
+ for (const reference of wanted.values()) {
369
+ const rows = existing.all(space, reference.from, reference.to, reference.kind) as Array<{ origin: Origin }>;
370
+ if (rows.some((row) => row.origin === analyzer)) result.references.unchanged++;
371
+ else if (rows.length === 0 && referenceSuppressed(db, reference, space)) result.references.suppressed++;
372
+ // The same fact from the user, the LLM or another analyzer already holds.
373
+ else if (rows.length === 0) {
374
+ change.insertReference({ space, ...reference, origin: analyzer });
375
+ result.references.added++;
376
+ }
377
+ }
378
+ }
379
+
380
+ /** Imports one analyzer result as one revision. Nothing is written when validation fails. */
381
+ export function importFindings(db: DatabaseSync, findings: NormalizedFindings, context: ImportContext = {}): ImportResult {
382
+ const coverage = validateFindings(findings);
383
+ return transaction(db, () => {
384
+ const change = new Change(db, { origin: findings.analyzer, ...context }, `import-${findings.analyzer}`);
385
+ const result: ImportResult = { revision: null, symbols: counts(), regions: counts(), references: counts(), conflicts: [] };
386
+ importSymbols(db, change, findings, coverage, result);
387
+ importRegions(db, change, findings, coverage, result);
388
+ importReferences(db, change, findings, coverage, result);
389
+ result.revision = change.revision;
390
+ return result;
391
+ });
392
+ }