@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.20
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 +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- package/skills/c64-docs/SKILL.md +137 -0
- package/skills/c64-docs/references/page-types.md +113 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +193 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +89 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -60
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +63 -0
- package/src/host-client/image.ts +66 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +203 -32
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +137 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -8
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
package/src/knowledge/history.ts
CHANGED
|
@@ -6,42 +6,72 @@ import type { DatabaseSync } from "node:sqlite";
|
|
|
6
6
|
import { KnowledgeError } from "./database.ts";
|
|
7
7
|
import {
|
|
8
8
|
COMMENT_COLUMNS,
|
|
9
|
+
MACRO_COLUMNS,
|
|
10
|
+
MACRO_USE_COLUMNS,
|
|
11
|
+
MODULE_COLUMNS,
|
|
12
|
+
OPERAND_COLUMNS,
|
|
13
|
+
PATCH_COLUMNS,
|
|
9
14
|
REFERENCE_COLUMNS,
|
|
10
15
|
REGION_COLUMNS,
|
|
16
|
+
resolveRevision,
|
|
17
|
+
SUPPRESSION_COLUMNS,
|
|
11
18
|
SYMBOL_COLUMNS,
|
|
12
19
|
toComment,
|
|
20
|
+
toMacro,
|
|
21
|
+
toMacroUse,
|
|
22
|
+
toModule,
|
|
23
|
+
toOperand,
|
|
24
|
+
toPatch,
|
|
13
25
|
toReference,
|
|
14
26
|
toRegion,
|
|
27
|
+
toSuppression,
|
|
15
28
|
toSymbol,
|
|
16
29
|
type CommentRow,
|
|
30
|
+
type MacroRow,
|
|
31
|
+
type MacroUseRow,
|
|
32
|
+
type ModuleRow,
|
|
33
|
+
type OperandRow,
|
|
34
|
+
type PatchRow,
|
|
17
35
|
type ReferenceRow,
|
|
18
36
|
type RegionRow,
|
|
37
|
+
type SuppressionRow,
|
|
19
38
|
type SymbolRow,
|
|
20
39
|
} from "./read.ts";
|
|
21
|
-
import type
|
|
40
|
+
import { DEFAULT_SPACE, type EvidenceKind, type Origin, type Space } from "./schema.ts";
|
|
41
|
+
import { compareText } from "./text.ts";
|
|
42
|
+
import type { Evidence } from "./write.ts";
|
|
22
43
|
|
|
23
44
|
export interface Revision {
|
|
24
|
-
id:
|
|
45
|
+
id: string;
|
|
25
46
|
createdAt: string;
|
|
26
47
|
origin: Origin;
|
|
27
48
|
operation: string;
|
|
28
49
|
description?: string;
|
|
29
50
|
inputHash?: string;
|
|
30
51
|
toolVersion?: string;
|
|
52
|
+
/** What the change rests on (D42). */
|
|
53
|
+
evidence?: Evidence[];
|
|
31
54
|
}
|
|
32
55
|
|
|
33
56
|
/** One knowledge row with its validity: from its first revision until the revision that closed it. */
|
|
34
57
|
export type HistoryEntry =
|
|
35
|
-
| { entity: "symbol"; row: SymbolRow; from:
|
|
36
|
-
| { entity: "
|
|
37
|
-
| { entity: "
|
|
38
|
-
| { entity: "
|
|
58
|
+
| { entity: "symbol"; row: SymbolRow; from: string; to?: string }
|
|
59
|
+
| { entity: "analyzer-name"; row: SymbolRow; from: string; to?: string }
|
|
60
|
+
| { entity: "region"; row: RegionRow; from: string; to?: string }
|
|
61
|
+
| { entity: "comment"; row: CommentRow; from: string; to?: string }
|
|
62
|
+
| { entity: "reference"; row: ReferenceRow; from: string; to?: string }
|
|
63
|
+
| { entity: "suppression"; row: SuppressionRow; from: string; to?: string }
|
|
64
|
+
| { entity: "module"; row: ModuleRow; from: string; to?: string }
|
|
65
|
+
| { entity: "operand"; row: OperandRow; from: string; to?: string }
|
|
66
|
+
| { entity: "patch"; row: PatchRow; from: string; to?: string }
|
|
67
|
+
| { entity: "macro"; row: MacroRow; from: string; to?: string }
|
|
68
|
+
| { entity: "macro-use"; row: MacroUseRow; from: string; to?: string };
|
|
39
69
|
|
|
40
70
|
type Raw = Record<string, unknown>;
|
|
41
71
|
|
|
42
72
|
function toRevision(row: Raw): Revision {
|
|
43
73
|
const revision: Revision = {
|
|
44
|
-
id: row.id as
|
|
74
|
+
id: row.id as string,
|
|
45
75
|
createdAt: row.created_at as string,
|
|
46
76
|
origin: row.origin as Origin,
|
|
47
77
|
operation: row.operation as string,
|
|
@@ -53,47 +83,86 @@ function toRevision(row: Raw): Revision {
|
|
|
53
83
|
}
|
|
54
84
|
|
|
55
85
|
function entry<E extends HistoryEntry["entity"]>(entity: E, row: unknown, raw: Raw): HistoryEntry {
|
|
56
|
-
const result = { entity, row, from: raw.valid_from_revision as
|
|
57
|
-
if (raw.valid_to_revision !== null) result.to = raw.valid_to_revision as
|
|
86
|
+
const result = { entity, row, from: raw.valid_from_revision as string } as HistoryEntry;
|
|
87
|
+
if (raw.valid_to_revision !== null) result.to = raw.valid_to_revision as string;
|
|
58
88
|
return result;
|
|
59
89
|
}
|
|
60
90
|
|
|
91
|
+
/** Revision ids sort in time order as text (D69); a current row sorts after every closed one. */
|
|
61
92
|
function bySequence(a: HistoryEntry, b: HistoryEntry): number {
|
|
62
|
-
return a.from
|
|
93
|
+
return compareText(a.from, b.from) || compareText(a.to ?? "~", b.to ?? "~");
|
|
63
94
|
}
|
|
64
95
|
|
|
65
|
-
/** Every row, current or closed, that concerns one address, oldest first. */
|
|
66
|
-
export function historyAt(db: DatabaseSync | undefined, address: number): HistoryEntry[] {
|
|
96
|
+
/** Every row, current or closed, that concerns one address of a space, oldest first. */
|
|
97
|
+
export function historyAt(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): HistoryEntry[] {
|
|
67
98
|
if (db === undefined) return [];
|
|
68
99
|
const validity = ", valid_to_revision";
|
|
69
100
|
const entries: HistoryEntry[] = [];
|
|
70
|
-
for (const raw of db.prepare(`SELECT ${SYMBOL_COLUMNS}${validity} FROM symbols WHERE address = ?`).all(address) as Raw[]) entries.push(entry("symbol", toSymbol(raw), raw));
|
|
71
|
-
for (const raw of db.prepare(`SELECT ${
|
|
101
|
+
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));
|
|
102
|
+
for (const raw of db.prepare(`SELECT ${SYMBOL_COLUMNS}${validity} FROM analyzer_names WHERE space = ? AND address = ?`).all(space, address) as Raw[]) entries.push(entry("analyzer-name", toSymbol(raw), raw));
|
|
103
|
+
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[]) {
|
|
72
104
|
entries.push(entry("region", toRegion(raw), raw));
|
|
73
105
|
}
|
|
74
|
-
for (const raw of db.prepare(`SELECT ${COMMENT_COLUMNS}${validity} FROM comments WHERE address = ?`).all(address) as Raw[]) entries.push(entry("comment", toComment(raw), raw));
|
|
75
|
-
for (const raw of db.prepare(`SELECT ${REFERENCE_COLUMNS}${validity} FROM "references" WHERE from_address = ? OR to_address =
|
|
106
|
+
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));
|
|
107
|
+
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[]) {
|
|
76
108
|
entries.push(entry("reference", toReference(raw), raw));
|
|
77
109
|
}
|
|
110
|
+
for (const raw of db
|
|
111
|
+
.prepare(`SELECT ${SUPPRESSION_COLUMNS}${validity} FROM suppressions WHERE space = ? AND ((start_address <= ? AND end_address >= ?) OR to_address = ?)`)
|
|
112
|
+
.all(space, address, address, address) as Raw[]) {
|
|
113
|
+
entries.push(entry("suppression", toSuppression(raw), raw));
|
|
114
|
+
}
|
|
115
|
+
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));
|
|
116
|
+
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));
|
|
117
|
+
for (const raw of db.prepare(`SELECT ${PATCH_COLUMNS}${validity} FROM patches WHERE space = ? AND start_address <= ? AND end_address >= ?`).all(space, address, address) as Raw[]) {
|
|
118
|
+
entries.push(entry("patch", toPatch(raw), raw));
|
|
119
|
+
}
|
|
120
|
+
for (const raw of db.prepare(`SELECT ${MACRO_USE_COLUMNS}${validity} FROM macro_uses WHERE space = ? AND start_address <= ? AND end_address >= ?`).all(space, address, address) as Raw[]) {
|
|
121
|
+
entries.push(entry("macro-use", toMacroUse(raw), raw));
|
|
122
|
+
}
|
|
78
123
|
return entries.sort(bySequence);
|
|
79
124
|
}
|
|
80
125
|
|
|
81
|
-
/** Every
|
|
82
|
-
export function
|
|
126
|
+
/** Every row of a macro with this name, and every use of it, oldest first. */
|
|
127
|
+
export function historyOfMacro(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): HistoryEntry[] {
|
|
128
|
+
if (db === undefined) return [];
|
|
129
|
+
const macros = (db.prepare(`SELECT ${MACRO_COLUMNS}, valid_to_revision FROM macros WHERE space = ? AND name = ?`).all(space, name) as Raw[]).map((raw) => entry("macro", toMacro(raw), raw));
|
|
130
|
+
const uses = (db.prepare(`SELECT ${MACRO_USE_COLUMNS}, valid_to_revision FROM macro_uses WHERE space = ? AND macro = ?`).all(space, name) as Raw[]).map((raw) => entry("macro-use", toMacroUse(raw), raw));
|
|
131
|
+
return [...macros, ...uses].sort(bySequence);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Every symbol row of a space that ever had this name, oldest first: a name can move between addresses. */
|
|
135
|
+
export function historyNamed(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): HistoryEntry[] {
|
|
83
136
|
if (db === undefined) return [];
|
|
84
|
-
return (db.prepare(`SELECT ${SYMBOL_COLUMNS}, valid_to_revision FROM symbols WHERE name = ?`).all(name) as Raw[])
|
|
137
|
+
return (db.prepare(`SELECT ${SYMBOL_COLUMNS}, valid_to_revision FROM symbols WHERE space = ? AND name = ?`).all(space, name) as Raw[])
|
|
85
138
|
.map((raw) => entry("symbol", toSymbol(raw), raw))
|
|
86
139
|
.sort(bySequence);
|
|
87
140
|
}
|
|
88
141
|
|
|
142
|
+
/** Why a revision was written and what it rests on: its description and its evidence, when it has them. */
|
|
143
|
+
export function revisionNote(db: DatabaseSync | undefined, id: string): { reason?: string; evidence?: Evidence[] } {
|
|
144
|
+
const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
|
|
145
|
+
if (db === undefined || raw === undefined) return {};
|
|
146
|
+
const { description, evidence } = withEvidence(db, toRevision(raw));
|
|
147
|
+
return { ...(description === undefined ? {} : { reason: description }), ...(evidence === undefined ? {} : { evidence }) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The revision with its evidence, when it has any. */
|
|
151
|
+
function withEvidence(db: DatabaseSync, revision: Revision): Revision {
|
|
152
|
+
const rows = db.prepare("SELECT kind, ref, sha256 FROM evidence WHERE revision = ? ORDER BY id").all(revision.id) as Raw[];
|
|
153
|
+
if (rows.length === 0) return revision;
|
|
154
|
+
const evidence = rows.map((row): Evidence => ({ kind: row.kind as EvidenceKind, ref: row.ref as string, ...(row.sha256 === null ? {} : { sha256: row.sha256 as string }) }));
|
|
155
|
+
return { ...revision, evidence };
|
|
156
|
+
}
|
|
157
|
+
|
|
89
158
|
/** Revisions, newest first; with `before`, only revisions older than it. */
|
|
90
|
-
export function revisions(db: DatabaseSync | undefined, options: { limit?: number; before?:
|
|
159
|
+
export function revisions(db: DatabaseSync | undefined, options: { limit?: number; before?: string } = {}): Revision[] {
|
|
91
160
|
if (db === undefined) return [];
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
.prepare("SELECT * FROM revisions
|
|
95
|
-
.all(options.before
|
|
96
|
-
).map(toRevision);
|
|
161
|
+
const rows =
|
|
162
|
+
options.before === undefined
|
|
163
|
+
? db.prepare("SELECT * FROM revisions ORDER BY id DESC LIMIT ?").all(options.limit ?? 50)
|
|
164
|
+
: db.prepare("SELECT * FROM revisions WHERE id < ? ORDER BY id DESC LIMIT ?").all(resolveRevision(db, options.before), options.limit ?? 50);
|
|
165
|
+
return (rows as Raw[]).map((row) => withEvidence(db, toRevision(row)));
|
|
97
166
|
}
|
|
98
167
|
|
|
99
168
|
export interface RevisionChanges {
|
|
@@ -105,14 +174,22 @@ export interface RevisionChanges {
|
|
|
105
174
|
}
|
|
106
175
|
|
|
107
176
|
/** One revision and exactly what it changed. */
|
|
108
|
-
export function revision(db: DatabaseSync | undefined,
|
|
177
|
+
export function revision(db: DatabaseSync | undefined, text: string): RevisionChanges {
|
|
178
|
+
const id = resolveRevision(db, text);
|
|
109
179
|
const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
|
|
110
180
|
if (db === undefined || raw === undefined) throw new KnowledgeError("not-found", `There is no revision ${id}.`);
|
|
111
181
|
const tables = [
|
|
112
182
|
["symbol", "symbols", SYMBOL_COLUMNS, toSymbol],
|
|
183
|
+
["analyzer-name", "analyzer_names", SYMBOL_COLUMNS, toSymbol],
|
|
113
184
|
["region", "regions", REGION_COLUMNS, toRegion],
|
|
114
185
|
["comment", "comments", COMMENT_COLUMNS, toComment],
|
|
115
186
|
["reference", '"references"', REFERENCE_COLUMNS, toReference],
|
|
187
|
+
["suppression", "suppressions", SUPPRESSION_COLUMNS, toSuppression],
|
|
188
|
+
["module", "modules", MODULE_COLUMNS, toModule],
|
|
189
|
+
["operand", "operands", OPERAND_COLUMNS, toOperand],
|
|
190
|
+
["patch", "patches", PATCH_COLUMNS, toPatch],
|
|
191
|
+
["macro", "macros", MACRO_COLUMNS, toMacro],
|
|
192
|
+
["macro-use", "macro_uses", MACRO_USE_COLUMNS, toMacroUse],
|
|
116
193
|
] as const;
|
|
117
194
|
const added: HistoryEntry[] = [];
|
|
118
195
|
const closed: HistoryEntry[] = [];
|
|
@@ -124,5 +201,6 @@ export function revision(db: DatabaseSync | undefined, id: number): RevisionChan
|
|
|
124
201
|
closed.push(entry(entity, map(row), row));
|
|
125
202
|
}
|
|
126
203
|
}
|
|
127
|
-
return { revision: toRevision(raw), added, closed };
|
|
204
|
+
return { revision: withEvidence(db, toRevision(raw)), added, closed };
|
|
128
205
|
}
|
|
206
|
+
|
package/src/knowledge/import.ts
CHANGED
|
@@ -6,37 +6,40 @@
|
|
|
6
6
|
// that the snapshot no longer holds are closed. Rows of the user, the LLM or
|
|
7
7
|
// another analyzer are never replaced: a finding that contradicts one is
|
|
8
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.
|
|
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.
|
|
10
12
|
|
|
11
13
|
import { createHash } from "node:crypto";
|
|
12
14
|
import type { DatabaseSync } from "node:sqlite";
|
|
13
15
|
|
|
14
16
|
import { KnowledgeError, transaction } from "./database.ts";
|
|
15
17
|
import {
|
|
18
|
+
analyzerNamed,
|
|
16
19
|
REFERENCE_COLUMNS,
|
|
20
|
+
referenceSuppressed,
|
|
17
21
|
regionsOverlapping,
|
|
22
|
+
suppressionsOverlapping,
|
|
18
23
|
SYMBOL_COLUMNS,
|
|
19
24
|
symbolAt,
|
|
20
|
-
symbolNamed,
|
|
21
25
|
toReference,
|
|
22
26
|
toSymbol,
|
|
23
27
|
type ReferenceRow,
|
|
24
28
|
type RegionRow,
|
|
25
29
|
type SymbolRow,
|
|
26
30
|
} from "./read.ts";
|
|
27
|
-
import { REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type Origin, type ReferenceKind, type RegionType, type SymbolKind } from "./schema.ts";
|
|
28
|
-
import { Change, checkAddress, hex, SYMBOL_NAME } from "./write.ts";
|
|
31
|
+
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";
|
|
32
|
+
import { Change, checkAddress, checkSpace, hex, subtract, SYMBOL_NAME, type AddressRange, type Evidence } from "./write.ts";
|
|
29
33
|
|
|
30
|
-
export
|
|
31
|
-
export type Analyzer = (typeof ANALYZERS)[number];
|
|
34
|
+
export type { AddressRange };
|
|
32
35
|
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
end: number;
|
|
36
|
-
}
|
|
36
|
+
export const ANALYZERS = ["dxa", "ghidra", "heuristic"] as const satisfies readonly Origin[];
|
|
37
|
+
export type Analyzer = (typeof ANALYZERS)[number];
|
|
37
38
|
|
|
38
39
|
export interface NormalizedFindings {
|
|
39
40
|
analyzer: Analyzer;
|
|
41
|
+
/** The address space that the analyzed image belongs to (D53); the C64 when absent. */
|
|
42
|
+
space?: Space;
|
|
40
43
|
/** The address ranges the analyzer examined; it makes claims only there. */
|
|
41
44
|
coverage: AddressRange[];
|
|
42
45
|
/** A category is authoritative when its findings are complete in coverage, so absence means "no longer asserted". */
|
|
@@ -45,16 +48,20 @@ export interface NormalizedFindings {
|
|
|
45
48
|
regions: Array<{ start: number; end: number; type: RegionType }>;
|
|
46
49
|
/** Owned and covered by their source address. */
|
|
47
50
|
references: Array<{ from: number; to: number; kind: ReferenceKind }>;
|
|
51
|
+
/** True for an address where a coverage run executed an instruction start (D58). */
|
|
52
|
+
executed?: (address: number) => boolean;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
export interface ImportContext {
|
|
51
56
|
/** The revision the caller read; the import is refused if knowledge changed since. */
|
|
52
|
-
expectedRevision?:
|
|
57
|
+
expectedRevision?: string;
|
|
53
58
|
description?: string;
|
|
54
59
|
/** The SHA-256 of the analyzed image, as lowercase hex. */
|
|
55
60
|
inputHash?: string;
|
|
56
61
|
/** The analyzer's own version line. */
|
|
57
62
|
toolVersion?: string;
|
|
63
|
+
/** What the import rests on besides the image, for example the coverage files. */
|
|
64
|
+
evidence?: Evidence[];
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/** What a revision records about one analyzer run: the hash of the image bytes and, when the analyzer tells it, its version. */
|
|
@@ -78,20 +85,25 @@ export interface ImportCounts {
|
|
|
78
85
|
changed: number;
|
|
79
86
|
retired: number;
|
|
80
87
|
unchanged: number;
|
|
88
|
+
/** Findings at a place that a user/LLM removal blocks (D36); not written. */
|
|
89
|
+
suppressed: number;
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
export interface ImportResult {
|
|
84
93
|
/** The new revision, or null when nothing changed. */
|
|
85
|
-
revision:
|
|
94
|
+
revision: string | null;
|
|
95
|
+
/** The analyzer names (D74). */
|
|
96
|
+
names: ImportCounts;
|
|
97
|
+
/** The analyzer's own rows from before D74 that the run closed in symbols: only `retired` counts. */
|
|
86
98
|
symbols: ImportCounts;
|
|
87
99
|
regions: ImportCounts;
|
|
88
100
|
references: ImportCounts;
|
|
89
101
|
conflicts: ImportConflict[];
|
|
90
102
|
}
|
|
91
103
|
|
|
92
|
-
const isCode = (value: SymbolKind | RegionType) => value === "label" || value === "routine" ||
|
|
104
|
+
const isCode = (value: SymbolKind | RegionType) => value === "label" || value === "routine" || (CODE_REGION_TYPES as readonly string[]).includes(value);
|
|
93
105
|
|
|
94
|
-
const counts = (): ImportCounts => ({ added: 0, changed: 0, retired: 0, unchanged: 0 });
|
|
106
|
+
const counts = (): ImportCounts => ({ added: 0, changed: 0, retired: 0, unchanged: 0, suppressed: 0 });
|
|
95
107
|
|
|
96
108
|
function invalid(message: string): never {
|
|
97
109
|
throw new KnowledgeError("invalid-input", `The analyzer result is not usable: ${message} Nothing was imported.`);
|
|
@@ -109,20 +121,6 @@ function mergeCoverage(coverage: AddressRange[]): AddressRange[] {
|
|
|
109
121
|
return merged;
|
|
110
122
|
}
|
|
111
123
|
|
|
112
|
-
/** Parts of `range` that no range in `holes` covers. */
|
|
113
|
-
function subtract(range: AddressRange, holes: AddressRange[]): AddressRange[] {
|
|
114
|
-
let pieces = [range];
|
|
115
|
-
for (const hole of holes) {
|
|
116
|
-
pieces = pieces.flatMap((piece) => {
|
|
117
|
-
if (hole.end < piece.start || hole.start > piece.end) return [piece];
|
|
118
|
-
const left = hole.start > piece.start ? [{ start: piece.start, end: hole.start - 1 }] : [];
|
|
119
|
-
const right = hole.end < piece.end ? [{ start: hole.end + 1, end: piece.end }] : [];
|
|
120
|
-
return [...left, ...right];
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
return pieces;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
124
|
/**
|
|
127
125
|
* Checks complete findings before anything is written. Every finding
|
|
128
126
|
* lies in coverage, names are valid and unique, regions do not overlap.
|
|
@@ -130,6 +128,7 @@ function subtract(range: AddressRange, holes: AddressRange[]): AddressRange[] {
|
|
|
130
128
|
*/
|
|
131
129
|
export function validateFindings(findings: NormalizedFindings): AddressRange[] {
|
|
132
130
|
if (!(ANALYZERS as readonly string[]).includes(findings.analyzer)) invalid(`the analyzer must be one of ${ANALYZERS.join(", ")}.`);
|
|
131
|
+
checkSpace(findings);
|
|
133
132
|
if (!Array.isArray(findings.coverage) || findings.coverage.length === 0) invalid("the result has no coverage.");
|
|
134
133
|
for (const range of findings.coverage) {
|
|
135
134
|
checkAddress(range.start, "coverage start");
|
|
@@ -166,76 +165,126 @@ export function validateFindings(findings: NormalizedFindings): AddressRange[] {
|
|
|
166
165
|
checkAddress(reference.to, "reference target");
|
|
167
166
|
if (!covering(reference.from, reference.from)) invalid(`the reference from ${hex(reference.from)} is outside the coverage.`);
|
|
168
167
|
if (!(REFERENCE_KINDS as readonly string[]).includes(reference.kind)) invalid(`the reference kind ${reference.kind} is not known.`);
|
|
168
|
+
if (reference.kind === POINTER_KIND) invalid(`only an operand expression writes a pointer reference (D65), not an analyzer.`);
|
|
169
169
|
}
|
|
170
170
|
return coverage;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
function ownSymbols(db: DatabaseSync, analyzer: Analyzer, range: AddressRange): SymbolRow[] {
|
|
173
|
+
function ownSymbols(db: DatabaseSync, analyzer: Analyzer, range: AddressRange, space: Space): SymbolRow[] {
|
|
174
174
|
return db
|
|
175
|
-
.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_symbols WHERE origin = ? AND address BETWEEN ? AND ? ORDER BY address`)
|
|
176
|
-
.all(analyzer, range.start, range.end)
|
|
175
|
+
.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_symbols WHERE space = ? AND origin = ? AND address BETWEEN ? AND ? ORDER BY address`)
|
|
176
|
+
.all(space, analyzer, range.start, range.end)
|
|
177
177
|
.map((row) => toSymbol(row as Record<string, unknown>));
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
function ownReferences(db: DatabaseSync, analyzer: Analyzer, range: AddressRange): ReferenceRow[] {
|
|
180
|
+
function ownReferences(db: DatabaseSync, analyzer: Analyzer, range: AddressRange, space: Space): ReferenceRow[] {
|
|
181
181
|
return db
|
|
182
|
-
.prepare(`SELECT ${REFERENCE_COLUMNS} FROM current_references WHERE origin = ? AND from_address BETWEEN ? AND ? ORDER BY from_address`)
|
|
183
|
-
.all(analyzer, range.start, range.end)
|
|
182
|
+
.prepare(`SELECT ${REFERENCE_COLUMNS} FROM current_references WHERE space = ? AND origin = ? AND from_address BETWEEN ? AND ? ORDER BY from_address`)
|
|
183
|
+
.all(space, analyzer, range.start, range.end)
|
|
184
184
|
.map((row) => toReference(row as Record<string, unknown>));
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
function
|
|
187
|
+
function ownNames(db: DatabaseSync, analyzer: Analyzer, range: AddressRange, space: Space): SymbolRow[] {
|
|
188
|
+
return db
|
|
189
|
+
.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_analyzer_names WHERE space = ? AND origin = ? AND address BETWEEN ? AND ? ORDER BY address`)
|
|
190
|
+
.all(space, analyzer, range.start, range.end)
|
|
191
|
+
.map((row) => toSymbol(row as Record<string, unknown>));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The names of an analyzer are second names (D74): they go to analyzer_names,
|
|
196
|
+
* one for each analyzer at an address, and never to symbols. A run replaces
|
|
197
|
+
* the analyzer's own names in its coverage, and closes the analyzer's old rows
|
|
198
|
+
* in symbols there (knowledge from before D74). A name that the user or the LLM
|
|
199
|
+
* gave the address already is not kept twice. A code or data kind that
|
|
200
|
+
* disagrees with the name of the address is a conflict.
|
|
201
|
+
*/
|
|
202
|
+
function importNames(db: DatabaseSync, change: Change, findings: NormalizedFindings, coverage: AddressRange[], result: ImportResult): void {
|
|
188
203
|
const { analyzer } = findings;
|
|
204
|
+
const space = findings.space ?? DEFAULT_SPACE;
|
|
205
|
+
for (const range of coverage) {
|
|
206
|
+
for (const own of ownSymbols(db, analyzer, range, space)) {
|
|
207
|
+
change.close("symbols", "space = ? AND address = ? AND origin = ?", space, own.address, analyzer);
|
|
208
|
+
result.symbols.retired++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
189
211
|
const wanted = new Map(findings.symbols.map((symbol) => [symbol.address, symbol]));
|
|
190
212
|
const settled = new Set<number>();
|
|
191
213
|
const replaced = new Set<number>();
|
|
192
|
-
// Close first, so a name can move between addresses within one import.
|
|
193
214
|
for (const range of coverage) {
|
|
194
|
-
for (const own of
|
|
215
|
+
for (const own of ownNames(db, analyzer, range, space)) {
|
|
195
216
|
const finding = wanted.get(own.address);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
result.symbols.retired++;
|
|
200
|
-
} else if (finding.name === own.name && finding.kind === own.kind) {
|
|
217
|
+
// A seeded run gives back the name of the user or the LLM: the analyzer's own name there stays.
|
|
218
|
+
const echo = finding !== undefined && symbolAt(db, own.address, space)?.name === finding.name;
|
|
219
|
+
if (echo || (finding !== undefined && finding.name === own.name && finding.kind === own.kind)) {
|
|
201
220
|
settled.add(own.address);
|
|
202
|
-
result.
|
|
203
|
-
|
|
204
|
-
change.close("symbols", "address = ?", own.address);
|
|
205
|
-
replaced.add(own.address);
|
|
221
|
+
result.names.unchanged++;
|
|
222
|
+
continue;
|
|
206
223
|
}
|
|
224
|
+
change.close("analyzer_names", "space = ? AND address = ? AND origin = ?", space, own.address, analyzer);
|
|
225
|
+
if (finding === undefined) result.names.retired++;
|
|
226
|
+
else replaced.add(own.address);
|
|
207
227
|
}
|
|
208
228
|
}
|
|
209
229
|
for (const finding of findings.symbols) {
|
|
230
|
+
const current = symbolAt(db, finding.address, space);
|
|
231
|
+
if (current !== undefined && isCode(current.kind) !== isCode(finding.kind)) {
|
|
232
|
+
result.conflicts.push({ category: "symbol", address: finding.address, reason: "kind", current, finding: { name: finding.name, kind: finding.kind } });
|
|
233
|
+
}
|
|
210
234
|
if (settled.has(finding.address)) continue;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (isCode(current.kind) !== isCode(finding.kind)) {
|
|
215
|
-
result.conflicts.push({ category: "symbol", address: finding.address, reason: "kind", current, finding: { name: finding.name, kind: finding.kind } });
|
|
216
|
-
}
|
|
235
|
+
if (current?.name === finding.name) {
|
|
236
|
+
// The analyzer only gives back the name of the address.
|
|
237
|
+
if (replaced.has(finding.address)) result.names.retired++;
|
|
217
238
|
continue;
|
|
218
239
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
result.symbols.retired++;
|
|
224
|
-
} else if (owner !== undefined) {
|
|
225
|
-
result.conflicts.push({ category: "symbol", address: finding.address, reason: "name-taken", current: owner, finding: { name: finding.name, kind: finding.kind } });
|
|
226
|
-
if (replaced.has(finding.address)) result.symbols.retired++;
|
|
227
|
-
continue;
|
|
240
|
+
// The analyzer moved its own name from an address outside the coverage.
|
|
241
|
+
for (const moved of analyzerNamed(db, finding.name, space).filter((row) => row.origin === analyzer)) {
|
|
242
|
+
change.close("analyzer_names", "space = ? AND address = ? AND origin = ?", space, moved.address, analyzer);
|
|
243
|
+
result.names.retired++;
|
|
228
244
|
}
|
|
229
|
-
change.
|
|
230
|
-
if (replaced.has(finding.address)) result.
|
|
231
|
-
else result.
|
|
245
|
+
change.insertAnalyzerName({ space, address: finding.address, name: finding.name, kind: finding.kind, origin: analyzer });
|
|
246
|
+
if (replaced.has(finding.address)) result.names.changed++;
|
|
247
|
+
else result.names.added++;
|
|
232
248
|
}
|
|
233
249
|
}
|
|
234
250
|
|
|
251
|
+
/** Analyzers whose regions a heuristic finding replaces where it lies (D44): user/LLM > heuristic > DXA/Ghidra. */
|
|
252
|
+
const YIELDS_TO_HEURISTIC = new Set<Origin>(["dxa", "ghidra"]);
|
|
253
|
+
/** Analyzers whose data gives way to a code finding over an executed instruction start (D58). */
|
|
254
|
+
const YIELDS_TO_EXECUTED_CODE = new Set<Origin>(["dxa", "ghidra"]);
|
|
255
|
+
|
|
256
|
+
/** True when an address in the range is an executed instruction start. */
|
|
257
|
+
function holdsExecuted(findings: NormalizedFindings, start: number, end: number): boolean {
|
|
258
|
+
if (findings.executed === undefined) return false;
|
|
259
|
+
for (let address = start; address <= end; address++) if (findings.executed(address)) return true;
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
235
263
|
function importRegions(db: DatabaseSync, change: Change, findings: NormalizedFindings, coverage: AddressRange[], result: ImportResult): void {
|
|
236
264
|
const { analyzer } = findings;
|
|
265
|
+
const space = findings.space ?? DEFAULT_SPACE;
|
|
266
|
+
if (analyzer === "heuristic") {
|
|
267
|
+
// An idiom is specific evidence about its bytes: the DXA or Ghidra region there gives way, its parts outside stay.
|
|
268
|
+
for (const region of findings.regions) {
|
|
269
|
+
for (const row of regionsOverlapping(db, region.start, region.end, space).filter((candidate) => YIELDS_TO_HEURISTIC.has(candidate.origin))) {
|
|
270
|
+
change.close("regions", "space = ? AND start_address = ? AND end_address = ? AND origin = ?", space, row.start, row.end, row.origin);
|
|
271
|
+
for (const piece of subtract(row, [region])) change.insertRegion({ space, ...piece, type: row.type, ...(row.element === undefined ? {} : { element: row.element }), origin: row.origin });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// 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.
|
|
276
|
+
for (const region of findings.regions.filter((candidate) => isCode(candidate.type))) {
|
|
277
|
+
for (const row of regionsOverlapping(db, region.start, region.end, space)) {
|
|
278
|
+
if (row.origin === analyzer || !YIELDS_TO_EXECUTED_CODE.has(row.origin) || isCode(row.type) || row.type === "dual-use") continue;
|
|
279
|
+
if (!holdsExecuted(findings, Math.max(row.start, region.start), Math.min(row.end, region.end))) continue;
|
|
280
|
+
change.close("regions", "space = ? AND start_address = ? AND end_address = ? AND origin = ?", space, row.start, row.end, row.origin);
|
|
281
|
+
for (const piece of subtract(row, [region])) change.insertRegion({ space, ...piece, type: row.type, ...(row.element === undefined ? {} : { element: row.element }), origin: row.origin });
|
|
282
|
+
result.regions.retired++;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
237
285
|
const conflictWith = (row: RegionRow, region: { start: number; end: number; type: RegionType }) => {
|
|
238
|
-
|
|
286
|
+
// dual-use bytes are code at one time and data at another: neither finding contradicts them (D42).
|
|
287
|
+
if (isCode(row.type) === isCode(region.type) || row.type === "dual-use") return;
|
|
239
288
|
result.conflicts.push({
|
|
240
289
|
category: "region",
|
|
241
290
|
start: Math.max(row.start, region.start),
|
|
@@ -249,7 +298,9 @@ function importRegions(db: DatabaseSync, change: Change, findings: NormalizedFin
|
|
|
249
298
|
// Retire nothing the snapshot leaves out. Fill addresses that no region covers yet, and
|
|
250
299
|
// give the analyzer's own regions under a finding the type that the analyzer finds now.
|
|
251
300
|
for (const region of findings.regions) {
|
|
252
|
-
const
|
|
301
|
+
const suppressed = suppressionsOverlapping(db, "region", region.start, region.end, space);
|
|
302
|
+
if (suppressed.length > 0) result.regions.suppressed++;
|
|
303
|
+
const current = regionsOverlapping(db, region.start, region.end, space);
|
|
253
304
|
const protectedRows = current.filter((row) => row.origin !== analyzer);
|
|
254
305
|
for (const row of protectedRows) conflictWith(row, region);
|
|
255
306
|
const own = current.filter((row) => row.origin === analyzer);
|
|
@@ -257,14 +308,14 @@ function importRegions(db: DatabaseSync, change: Change, findings: NormalizedFin
|
|
|
257
308
|
result.regions.unchanged += kept.length;
|
|
258
309
|
for (const row of own.filter((candidate) => candidate.type !== region.type)) {
|
|
259
310
|
// Close the row and keep its parts outside the finding.
|
|
260
|
-
change.close("regions", "start_address = ? AND end_address = ? AND origin = ?", row.start, row.end, analyzer);
|
|
261
|
-
if (row.start < region.start) change.insertRegion({ start: row.start, end: region.start - 1, type: row.type, origin: analyzer });
|
|
262
|
-
if (row.end > region.end) change.insertRegion({ start: region.end + 1, end: row.end, type: row.type, origin: analyzer });
|
|
311
|
+
change.close("regions", "space = ? AND start_address = ? AND end_address = ? AND origin = ?", space, row.start, row.end, analyzer);
|
|
312
|
+
if (row.start < region.start) change.insertRegion({ space, start: row.start, end: region.start - 1, type: row.type, origin: analyzer });
|
|
313
|
+
if (row.end > region.end) change.insertRegion({ space, start: region.end + 1, end: row.end, type: row.type, origin: analyzer });
|
|
263
314
|
result.regions.changed++;
|
|
264
315
|
}
|
|
265
|
-
const empty = subtract(region, current);
|
|
266
|
-
for (const piece of subtract(region, [...protectedRows, ...kept])) {
|
|
267
|
-
change.insertRegion({ ...piece, type: region.type, origin: analyzer });
|
|
316
|
+
const empty = subtract(region, [...current, ...suppressed]);
|
|
317
|
+
for (const piece of subtract(region, [...protectedRows, ...kept, ...suppressed])) {
|
|
318
|
+
change.insertRegion({ space, ...piece, type: region.type, origin: analyzer });
|
|
268
319
|
}
|
|
269
320
|
result.regions.added += empty.length;
|
|
270
321
|
}
|
|
@@ -272,14 +323,16 @@ function importRegions(db: DatabaseSync, change: Change, findings: NormalizedFin
|
|
|
272
323
|
}
|
|
273
324
|
|
|
274
325
|
for (const range of coverage) {
|
|
275
|
-
const current = regionsOverlapping(db, range.start, range.end);
|
|
326
|
+
const current = regionsOverlapping(db, range.start, range.end, space);
|
|
276
327
|
const protectedRows = current.filter((row) => row.origin !== analyzer);
|
|
277
328
|
const own = current.filter((row) => row.origin === analyzer);
|
|
278
329
|
// What the analyzer may own here: its regions minus protected rows.
|
|
279
330
|
const desired = new Map<string, { start: number; end: number; type: RegionType }>();
|
|
280
331
|
for (const region of findings.regions.filter((candidate) => candidate.start >= range.start && candidate.end <= range.end)) {
|
|
281
332
|
for (const row of protectedRows) if (row.start <= region.end && row.end >= region.start) conflictWith(row, region);
|
|
282
|
-
|
|
333
|
+
const suppressed = suppressionsOverlapping(db, "region", region.start, region.end, space);
|
|
334
|
+
if (suppressed.length > 0) result.regions.suppressed++;
|
|
335
|
+
for (const piece of subtract(region, [...protectedRows, ...suppressed])) desired.set(`${piece.start}-${piece.end}`, { ...piece, type: region.type });
|
|
283
336
|
}
|
|
284
337
|
const retiredRanges = new Set<string>();
|
|
285
338
|
for (const row of own) {
|
|
@@ -291,13 +344,13 @@ function importRegions(db: DatabaseSync, change: Change, findings: NormalizedFin
|
|
|
291
344
|
continue;
|
|
292
345
|
}
|
|
293
346
|
// Close the row and keep its parts outside the coverage.
|
|
294
|
-
change.close("regions", "start_address = ? AND end_address = ? AND origin = ?", row.start, row.end, analyzer);
|
|
295
|
-
if (row.start < range.start) change.insertRegion({ start: row.start, end: range.start - 1, type: row.type, origin: analyzer });
|
|
296
|
-
if (row.end > range.end) change.insertRegion({ start: range.end + 1, end: row.end, type: row.type, origin: analyzer });
|
|
347
|
+
change.close("regions", "space = ? AND start_address = ? AND end_address = ? AND origin = ?", space, row.start, row.end, analyzer);
|
|
348
|
+
if (row.start < range.start) change.insertRegion({ space, start: row.start, end: range.start - 1, type: row.type, origin: analyzer });
|
|
349
|
+
if (row.end > range.end) change.insertRegion({ space, start: range.end + 1, end: row.end, type: row.type, origin: analyzer });
|
|
297
350
|
retiredRanges.add(key);
|
|
298
351
|
}
|
|
299
352
|
for (const [key, piece] of desired) {
|
|
300
|
-
change.insertRegion({ ...piece, origin: analyzer });
|
|
353
|
+
change.insertRegion({ space, ...piece, origin: analyzer });
|
|
301
354
|
if (retiredRanges.delete(key)) result.regions.changed++;
|
|
302
355
|
else result.regions.added++;
|
|
303
356
|
}
|
|
@@ -307,23 +360,25 @@ function importRegions(db: DatabaseSync, change: Change, findings: NormalizedFin
|
|
|
307
360
|
|
|
308
361
|
function importReferences(db: DatabaseSync, change: Change, findings: NormalizedFindings, coverage: AddressRange[], result: ImportResult): void {
|
|
309
362
|
const { analyzer } = findings;
|
|
363
|
+
const space = findings.space ?? DEFAULT_SPACE;
|
|
310
364
|
const key = (reference: { from: number; to: number; kind: ReferenceKind }) => `${reference.from}|${reference.to}|${reference.kind}`;
|
|
311
365
|
const wanted = new Map(findings.references.map((reference) => [key(reference), reference]));
|
|
312
366
|
for (const range of coverage) {
|
|
313
|
-
for (const own of ownReferences(db, analyzer, range)) {
|
|
367
|
+
for (const own of ownReferences(db, analyzer, range, space)) {
|
|
314
368
|
if (wanted.has(key(own))) continue;
|
|
315
369
|
if (!findings.authoritative.references) continue;
|
|
316
|
-
change.close('"references"', "from_address = ? AND to_address = ? AND kind = ? AND origin = ?", own.from, own.to, own.kind, analyzer);
|
|
370
|
+
change.close('"references"', "space = ? AND from_address = ? AND to_address = ? AND kind = ? AND origin = ?", space, own.from, own.to, own.kind, analyzer);
|
|
317
371
|
result.references.retired++;
|
|
318
372
|
}
|
|
319
373
|
}
|
|
320
|
-
const existing = db.prepare(
|
|
374
|
+
const existing = db.prepare("SELECT origin FROM current_references WHERE space = ? AND from_address = ? AND to_address = ? AND kind = ?");
|
|
321
375
|
for (const reference of wanted.values()) {
|
|
322
|
-
const rows = existing.all(reference.from, reference.to, reference.kind) as Array<{ origin: Origin }>;
|
|
376
|
+
const rows = existing.all(space, reference.from, reference.to, reference.kind) as Array<{ origin: Origin }>;
|
|
323
377
|
if (rows.some((row) => row.origin === analyzer)) result.references.unchanged++;
|
|
378
|
+
else if (rows.length === 0 && referenceSuppressed(db, reference, space)) result.references.suppressed++;
|
|
324
379
|
// The same fact from the user, the LLM or another analyzer already holds.
|
|
325
380
|
else if (rows.length === 0) {
|
|
326
|
-
change.insertReference({ ...reference, origin: analyzer });
|
|
381
|
+
change.insertReference({ space, ...reference, origin: analyzer });
|
|
327
382
|
result.references.added++;
|
|
328
383
|
}
|
|
329
384
|
}
|
|
@@ -334,10 +389,11 @@ export function importFindings(db: DatabaseSync, findings: NormalizedFindings, c
|
|
|
334
389
|
const coverage = validateFindings(findings);
|
|
335
390
|
return transaction(db, () => {
|
|
336
391
|
const change = new Change(db, { origin: findings.analyzer, ...context }, `import-${findings.analyzer}`);
|
|
337
|
-
const result: ImportResult = { revision: null, symbols: counts(), regions: counts(), references: counts(), conflicts: [] };
|
|
338
|
-
|
|
392
|
+
const result: ImportResult = { revision: null, names: counts(), symbols: counts(), regions: counts(), references: counts(), conflicts: [] };
|
|
393
|
+
importNames(db, change, findings, coverage, result);
|
|
339
394
|
importRegions(db, change, findings, coverage, result);
|
|
340
395
|
importReferences(db, change, findings, coverage, result);
|
|
396
|
+
change.check();
|
|
341
397
|
result.revision = change.revision;
|
|
342
398
|
return result;
|
|
343
399
|
});
|