@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -59
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +5 -2
- package/skills/c64-assembler/scripts/assemble.ts +8 -1
- package/skills/c64-assembler/scripts/compare.ts +84 -0
- package/skills/c64-basic/SKILL.md +1 -2
- package/skills/c64-disk/SKILL.md +20 -6
- package/skills/c64-disk/scripts/containers.ts +124 -0
- package/skills/c64-disk/scripts/disk.ts +59 -5
- package/skills/c64-emulator/SKILL.md +11 -4
- package/skills/c64-knowledge/SKILL.md +53 -3
- package/skills/c64-knowledge/scripts/knowledge.ts +258 -29
- package/skills/c64-memory-map/SKILL.md +4 -1
- package/skills/c64-memory-map/scripts/memmap.ts +14 -3
- package/skills/c64-provenance/SKILL.md +37 -9
- package/skills/c64-provenance/scripts/compare.ts +60 -29
- package/skills/c64-provenance/scripts/provenance.ts +248 -19
- package/skills/c64-provenance/scripts/runtime.ts +179 -0
- package/skills/c64-provenance/scripts/separate.ts +132 -0
- package/skills/c64-reverse-engineering/SKILL.md +65 -11
- package/skills/c64-source/SKILL.md +119 -0
- package/skills/c64-source/scripts/generate.ts +402 -0
- package/skills/c64-source/scripts/loader.ts +320 -0
- package/skills/c64-source/scripts/opcodes.ts +61 -0
- package/skills/c64-source/scripts/source.ts +558 -0
- package/skills/c64-static-analysis/SKILL.md +27 -4
- package/skills/c64-static-analysis/scripts/analyze.ts +102 -13
- package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
- package/skills/c64-static-analysis/scripts/findings.ts +6 -4
- package/skills/c64-static-analysis/scripts/idioms.ts +144 -0
- package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
- package/skills/c64-testing/SKILL.md +27 -3
- package/skills/c64-testing/references/scenario-format.md +43 -3
- package/skills/c64-testing/scripts/run.ts +189 -0
- package/skills/c64-testing/scripts/scenario.ts +153 -135
- package/skills/c64-testing/scripts/test.ts +16 -6
- package/skills/c64-unpacker/SKILL.md +65 -14
- package/skills/c64-unpacker/scripts/evidence.ts +86 -21
- package/skills/c64-unpacker/scripts/loads.ts +174 -0
- package/skills/c64-unpacker/scripts/machine.ts +68 -0
- package/skills/c64-unpacker/scripts/unpack.ts +302 -48
- package/src/c64.ts +91 -0
- package/src/cli/status.ts +3 -1
- package/src/host/main.ts +10 -25
- package/src/host/server.ts +8 -40
- package/src/host/vice/adapter.ts +25 -10
- package/src/host/vice/session.ts +64 -20
- package/src/host-client/autostart.ts +45 -0
- package/src/host-client/connect.ts +4 -5
- package/src/host-client/coverage.ts +59 -0
- package/src/host-client/steps.ts +170 -0
- package/src/host-client/transfer.ts +29 -3
- package/src/host-client/vice-session.ts +42 -13
- package/src/knowledge/history.ts +40 -13
- package/src/knowledge/import.ts +72 -56
- package/src/knowledge/read.ts +203 -52
- package/src/knowledge/schema.ts +94 -12
- package/src/knowledge/write.ts +432 -123
- package/src/mcp/main.ts +1 -4
- package/src/mcp/output.ts +32 -0
- package/src/mcp/server.ts +4 -2
- package/src/mcp/tools/debug.ts +110 -24
- package/src/mcp/tools/input.ts +1 -0
- package/src/mcp/tools/media.ts +22 -6
- package/src/mcp/tools/memory.ts +57 -10
- package/src/mcp/tools/video.ts +9 -3
- package/src/native/discover.ts +5 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +30 -1
- package/{skills/c64-memory-map/scripts → src}/platform.ts +18 -2
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/vice.ts +72 -18
- package/src/script.ts +3 -2
package/src/knowledge/read.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Current-state knowledge queries. A project without a database
|
|
2
|
-
// reads as empty knowledge.
|
|
2
|
+
// reads as empty knowledge. Each query reads one address space (D53); the
|
|
3
|
+
// default is the C64.
|
|
3
4
|
|
|
4
5
|
import type { DatabaseSync } from "node:sqlite";
|
|
5
6
|
|
|
6
|
-
import type
|
|
7
|
+
import { DEFAULT_SPACE, type CommentPlacement, type Origin, type ReferenceKind, type RegionType, type Space, type SuppressionEntity, type SymbolKind } from "./schema.ts";
|
|
7
8
|
|
|
8
9
|
export interface SymbolRow {
|
|
10
|
+
space: Space;
|
|
9
11
|
address: number;
|
|
10
12
|
name: string;
|
|
11
13
|
kind: SymbolKind;
|
|
@@ -14,14 +16,18 @@ export interface SymbolRow {
|
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export interface RegionRow {
|
|
19
|
+
space: Space;
|
|
17
20
|
start: number;
|
|
18
21
|
end: number;
|
|
19
22
|
type: RegionType;
|
|
23
|
+
/** Bytes per element of a table; the length is a multiple of it (D42). */
|
|
24
|
+
element?: number;
|
|
20
25
|
origin: Origin;
|
|
21
26
|
revision: number;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
export interface CommentRow {
|
|
30
|
+
space: Space;
|
|
25
31
|
address: number;
|
|
26
32
|
placement: CommentPlacement;
|
|
27
33
|
text: string;
|
|
@@ -30,6 +36,7 @@ export interface CommentRow {
|
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
export interface ReferenceRow {
|
|
39
|
+
space: Space;
|
|
33
40
|
from: number;
|
|
34
41
|
to: number;
|
|
35
42
|
kind: ReferenceKind;
|
|
@@ -37,27 +44,87 @@ export interface ReferenceRow {
|
|
|
37
44
|
revision: number;
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
/** A place that a user/LLM removal blocks for every analyzer (D36). `to` and `kind` are set for a reference only. */
|
|
48
|
+
export interface SuppressionRow {
|
|
49
|
+
space: Space;
|
|
50
|
+
entity: SuppressionEntity;
|
|
51
|
+
start: number;
|
|
52
|
+
end: number;
|
|
53
|
+
to?: number;
|
|
54
|
+
kind?: ReferenceKind;
|
|
55
|
+
origin: Extract<Origin, "user" | "llm">;
|
|
56
|
+
revision: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Where a file of the generated source starts (D48): it runs to the next module or the end of its segment. */
|
|
60
|
+
export interface ModuleRow {
|
|
61
|
+
space: Space;
|
|
62
|
+
start: number;
|
|
63
|
+
file: string;
|
|
64
|
+
title?: string;
|
|
65
|
+
origin: Extract<Origin, "user" | "llm">;
|
|
66
|
+
revision: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
40
69
|
type Raw = Record<string, unknown>;
|
|
41
70
|
|
|
42
|
-
export const SYMBOL_COLUMNS = "address, name, kind, origin, valid_from_revision";
|
|
43
|
-
export const REGION_COLUMNS = "start_address, end_address, type, origin, valid_from_revision";
|
|
44
|
-
export const COMMENT_COLUMNS = "address, placement, text, origin, valid_from_revision";
|
|
45
|
-
export const REFERENCE_COLUMNS = "from_address, to_address, kind, origin, valid_from_revision";
|
|
71
|
+
export const SYMBOL_COLUMNS = "space, address, name, kind, origin, valid_from_revision";
|
|
72
|
+
export const REGION_COLUMNS = "space, start_address, end_address, type, element, origin, valid_from_revision";
|
|
73
|
+
export const COMMENT_COLUMNS = "space, address, placement, text, origin, valid_from_revision";
|
|
74
|
+
export const REFERENCE_COLUMNS = "space, from_address, to_address, kind, origin, valid_from_revision";
|
|
75
|
+
export const SUPPRESSION_COLUMNS = "space, entity, start_address, end_address, to_address, kind, origin, valid_from_revision";
|
|
76
|
+
export const MODULE_COLUMNS = "space, start_address, file, title, origin, valid_from_revision";
|
|
46
77
|
|
|
47
78
|
export function toSymbol(row: Raw): SymbolRow {
|
|
48
|
-
return { address: row.address as number, name: row.name as string, kind: row.kind as SymbolKind, origin: row.origin as Origin, revision: row.valid_from_revision as number };
|
|
79
|
+
return { space: row.space as Space, address: row.address as number, name: row.name as string, kind: row.kind as SymbolKind, origin: row.origin as Origin, revision: row.valid_from_revision as number };
|
|
49
80
|
}
|
|
50
81
|
|
|
51
82
|
export function toRegion(row: Raw): RegionRow {
|
|
52
|
-
|
|
83
|
+
const region: RegionRow = {
|
|
84
|
+
space: row.space as Space,
|
|
85
|
+
start: row.start_address as number,
|
|
86
|
+
end: row.end_address as number,
|
|
87
|
+
type: row.type as RegionType,
|
|
88
|
+
origin: row.origin as Origin,
|
|
89
|
+
revision: row.valid_from_revision as number,
|
|
90
|
+
};
|
|
91
|
+
if (row.element !== null && row.element !== undefined) region.element = row.element as number;
|
|
92
|
+
return region;
|
|
53
93
|
}
|
|
54
94
|
|
|
55
95
|
export function toComment(row: Raw): CommentRow {
|
|
56
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
space: row.space as Space,
|
|
98
|
+
address: row.address as number,
|
|
99
|
+
placement: row.placement as CommentPlacement,
|
|
100
|
+
text: row.text as string,
|
|
101
|
+
origin: row.origin as Origin,
|
|
102
|
+
revision: row.valid_from_revision as number,
|
|
103
|
+
};
|
|
57
104
|
}
|
|
58
105
|
|
|
59
106
|
export function toReference(row: Raw): ReferenceRow {
|
|
60
|
-
return { from: row.from_address as number, to: row.to_address as number, kind: row.kind as ReferenceKind, origin: row.origin as Origin, revision: row.valid_from_revision as number };
|
|
107
|
+
return { space: row.space as Space, from: row.from_address as number, to: row.to_address as number, kind: row.kind as ReferenceKind, origin: row.origin as Origin, revision: row.valid_from_revision as number };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function toSuppression(row: Raw): SuppressionRow {
|
|
111
|
+
const suppression: SuppressionRow = {
|
|
112
|
+
space: row.space as Space,
|
|
113
|
+
entity: row.entity as SuppressionEntity,
|
|
114
|
+
start: row.start_address as number,
|
|
115
|
+
end: row.end_address as number,
|
|
116
|
+
origin: row.origin as SuppressionRow["origin"],
|
|
117
|
+
revision: row.valid_from_revision as number,
|
|
118
|
+
};
|
|
119
|
+
if (row.to_address !== null) suppression.to = row.to_address as number;
|
|
120
|
+
if (row.kind !== null) suppression.kind = row.kind as ReferenceKind;
|
|
121
|
+
return suppression;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function toModule(row: Raw): ModuleRow {
|
|
125
|
+
const module: ModuleRow = { space: row.space as Space, start: row.start_address as number, file: row.file as string, origin: row.origin as ModuleRow["origin"], revision: row.valid_from_revision as number };
|
|
126
|
+
if (row.title !== null && row.title !== undefined) module.title = row.title as string;
|
|
127
|
+
return module;
|
|
61
128
|
}
|
|
62
129
|
|
|
63
130
|
export function currentRevision(db: DatabaseSync | undefined): number {
|
|
@@ -65,51 +132,90 @@ export function currentRevision(db: DatabaseSync | undefined): number {
|
|
|
65
132
|
return Number((db.prepare("SELECT value FROM meta WHERE key = 'current_revision'").get() as { value: string }).value);
|
|
66
133
|
}
|
|
67
134
|
|
|
68
|
-
export function symbolAt(db: DatabaseSync | undefined, address: number): SymbolRow | undefined {
|
|
69
|
-
const row = db?.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_symbols WHERE address = ?`).get(address) as Raw | undefined;
|
|
135
|
+
export function symbolAt(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): SymbolRow | undefined {
|
|
136
|
+
const row = db?.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_symbols WHERE space = ? AND address = ?`).get(space, address) as Raw | undefined;
|
|
70
137
|
return row === undefined ? undefined : toSymbol(row);
|
|
71
138
|
}
|
|
72
139
|
|
|
73
|
-
export function symbolNamed(db: DatabaseSync | undefined, name: string): SymbolRow | undefined {
|
|
74
|
-
const row = db?.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_symbols WHERE name = ?`).get(name) as Raw | undefined;
|
|
140
|
+
export function symbolNamed(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): SymbolRow | undefined {
|
|
141
|
+
const row = db?.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_symbols WHERE space = ? AND name = ?`).get(space, name) as Raw | undefined;
|
|
75
142
|
return row === undefined ? undefined : toSymbol(row);
|
|
76
143
|
}
|
|
77
144
|
|
|
78
145
|
/** Current regions that share at least one address with [start, end], in address order. */
|
|
79
|
-
export function regionsOverlapping(db: DatabaseSync | undefined, start: number, end: number): RegionRow[] {
|
|
146
|
+
export function regionsOverlapping(db: DatabaseSync | undefined, start: number, end: number, space: Space = DEFAULT_SPACE): RegionRow[] {
|
|
80
147
|
if (db === undefined) return [];
|
|
81
148
|
return db
|
|
82
|
-
.prepare(`SELECT ${REGION_COLUMNS} FROM current_regions WHERE start_address <= ? AND end_address >= ? ORDER BY start_address`)
|
|
83
|
-
.all(end, start)
|
|
149
|
+
.prepare(`SELECT ${REGION_COLUMNS} FROM current_regions WHERE space = ? AND start_address <= ? AND end_address >= ? ORDER BY start_address`)
|
|
150
|
+
.all(space, end, start)
|
|
84
151
|
.map((row) => toRegion(row as Raw));
|
|
85
152
|
}
|
|
86
153
|
|
|
87
|
-
export function commentsAt(db: DatabaseSync | undefined, address: number): CommentRow[] {
|
|
154
|
+
export function commentsAt(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): CommentRow[] {
|
|
88
155
|
if (db === undefined) return [];
|
|
89
156
|
return db
|
|
90
|
-
.prepare(`SELECT ${COMMENT_COLUMNS} FROM current_comments WHERE address = ? ORDER BY placement`)
|
|
91
|
-
.all(address)
|
|
157
|
+
.prepare(`SELECT ${COMMENT_COLUMNS} FROM current_comments WHERE space = ? AND address = ? ORDER BY placement`)
|
|
158
|
+
.all(space, address)
|
|
92
159
|
.map((row) => toComment(row as Raw));
|
|
93
160
|
}
|
|
94
161
|
|
|
95
|
-
export function referencesFrom(db: DatabaseSync | undefined, address: number): ReferenceRow[] {
|
|
162
|
+
export function referencesFrom(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): ReferenceRow[] {
|
|
96
163
|
if (db === undefined) return [];
|
|
97
164
|
return db
|
|
98
|
-
.prepare(`SELECT ${REFERENCE_COLUMNS} FROM current_references WHERE from_address = ? ORDER BY to_address, kind`)
|
|
99
|
-
.all(address)
|
|
165
|
+
.prepare(`SELECT ${REFERENCE_COLUMNS} FROM current_references WHERE space = ? AND from_address = ? ORDER BY to_address, kind`)
|
|
166
|
+
.all(space, address)
|
|
100
167
|
.map((row) => toReference(row as Raw));
|
|
101
168
|
}
|
|
102
169
|
|
|
103
|
-
export function referencesTo(db: DatabaseSync | undefined, address: number): ReferenceRow[] {
|
|
170
|
+
export function referencesTo(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): ReferenceRow[] {
|
|
104
171
|
if (db === undefined) return [];
|
|
105
172
|
return db
|
|
106
|
-
.prepare(`SELECT ${REFERENCE_COLUMNS} FROM current_references WHERE to_address = ? ORDER BY from_address, kind`)
|
|
107
|
-
.all(address)
|
|
173
|
+
.prepare(`SELECT ${REFERENCE_COLUMNS} FROM current_references WHERE space = ? AND to_address = ? ORDER BY from_address, kind`)
|
|
174
|
+
.all(space, address)
|
|
108
175
|
.map((row) => toReference(row as Raw));
|
|
109
176
|
}
|
|
110
177
|
|
|
178
|
+
/** Current suppressions of one entity that share at least one address with [start, end], in address order. */
|
|
179
|
+
export function suppressionsOverlapping(db: DatabaseSync | undefined, entity: SuppressionEntity, start: number, end: number, space: Space = DEFAULT_SPACE): SuppressionRow[] {
|
|
180
|
+
if (db === undefined) return [];
|
|
181
|
+
return db
|
|
182
|
+
.prepare(`SELECT ${SUPPRESSION_COLUMNS} FROM current_suppressions WHERE space = ? AND entity = ? AND start_address <= ? AND end_address >= ? ORDER BY start_address, to_address, kind`)
|
|
183
|
+
.all(space, entity, end, start)
|
|
184
|
+
.map((row) => toSuppression(row as Raw));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function symbolSuppressed(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): boolean {
|
|
188
|
+
return suppressionsOverlapping(db, "symbol", address, address, space).length > 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function referenceSuppressed(db: DatabaseSync | undefined, reference: { from: number; to: number; kind: ReferenceKind }, space: Space = DEFAULT_SPACE): boolean {
|
|
192
|
+
return suppressionsOverlapping(db, "reference", reference.from, reference.from, space).some((row) => row.to === reference.to && row.kind === reference.kind);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Current suppressions at an address: its symbol, the region range that holds it, references from or to it. */
|
|
196
|
+
export function suppressionsAt(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): SuppressionRow[] {
|
|
197
|
+
if (db === undefined) return [];
|
|
198
|
+
return db
|
|
199
|
+
.prepare(
|
|
200
|
+
`SELECT ${SUPPRESSION_COLUMNS} FROM current_suppressions WHERE space = ? AND ((start_address <= ? AND end_address >= ?) OR to_address = ?) ORDER BY entity, start_address, to_address, kind`,
|
|
201
|
+
)
|
|
202
|
+
.all(space, address, address, address)
|
|
203
|
+
.map((row) => toSuppression(row as Raw));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function moduleAt(db: DatabaseSync | undefined, start: number, space: Space = DEFAULT_SPACE): ModuleRow | undefined {
|
|
207
|
+
const row = db?.prepare(`SELECT ${MODULE_COLUMNS} FROM current_modules WHERE space = ? AND start_address = ?`).get(space, start) as Raw | undefined;
|
|
208
|
+
return row === undefined ? undefined : toModule(row);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function moduleWithFile(db: DatabaseSync | undefined, file: string, space: Space = DEFAULT_SPACE): ModuleRow | undefined {
|
|
212
|
+
const row = db?.prepare(`SELECT ${MODULE_COLUMNS} FROM current_modules WHERE space = ? AND file = ?`).get(space, file) as Raw | undefined;
|
|
213
|
+
return row === undefined ? undefined : toModule(row);
|
|
214
|
+
}
|
|
215
|
+
|
|
111
216
|
/** Everything currently known at one address: the main lookup for skills. */
|
|
112
217
|
export interface AddressKnowledge {
|
|
218
|
+
space: Space;
|
|
113
219
|
address: number;
|
|
114
220
|
symbol?: SymbolRow;
|
|
115
221
|
/** The current region that contains the address. */
|
|
@@ -117,20 +223,24 @@ export interface AddressKnowledge {
|
|
|
117
223
|
comments: CommentRow[];
|
|
118
224
|
referencesFrom: ReferenceRow[];
|
|
119
225
|
referencesTo: ReferenceRow[];
|
|
226
|
+
/** What user/LLM removals block here for the analyzers. */
|
|
227
|
+
suppressions: SuppressionRow[];
|
|
120
228
|
revision: number;
|
|
121
229
|
}
|
|
122
230
|
|
|
123
|
-
export function at(db: DatabaseSync | undefined, address: number): AddressKnowledge {
|
|
231
|
+
export function at(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): AddressKnowledge {
|
|
124
232
|
const result: AddressKnowledge = {
|
|
233
|
+
space,
|
|
125
234
|
address,
|
|
126
|
-
comments: commentsAt(db, address),
|
|
127
|
-
referencesFrom: referencesFrom(db, address),
|
|
128
|
-
referencesTo: referencesTo(db, address),
|
|
235
|
+
comments: commentsAt(db, address, space),
|
|
236
|
+
referencesFrom: referencesFrom(db, address, space),
|
|
237
|
+
referencesTo: referencesTo(db, address, space),
|
|
238
|
+
suppressions: suppressionsAt(db, address, space),
|
|
129
239
|
revision: currentRevision(db),
|
|
130
240
|
};
|
|
131
|
-
const symbol = symbolAt(db, address);
|
|
241
|
+
const symbol = symbolAt(db, address, space);
|
|
132
242
|
if (symbol !== undefined) result.symbol = symbol;
|
|
133
|
-
const [region] = regionsOverlapping(db, address, address);
|
|
243
|
+
const [region] = regionsOverlapping(db, address, address, space);
|
|
134
244
|
if (region !== undefined) result.region = region;
|
|
135
245
|
return result;
|
|
136
246
|
}
|
|
@@ -142,50 +252,91 @@ export interface Range {
|
|
|
142
252
|
|
|
143
253
|
const ALL: Range = { start: 0x0000, end: 0xffff };
|
|
144
254
|
|
|
145
|
-
|
|
255
|
+
/** Rows of these origins only; undefined keeps every origin (D42). */
|
|
256
|
+
export type OriginFilter = readonly Origin[] | undefined;
|
|
257
|
+
const kept = (origins: OriginFilter) => (row: { origin: Origin }) => origins === undefined || origins.includes(row.origin);
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
export function listSymbols(db: DatabaseSync | undefined, range: Range = ALL, kind?: SymbolKind, origins?: OriginFilter, space: Space = DEFAULT_SPACE): SymbolRow[] {
|
|
146
261
|
if (db === undefined) return [];
|
|
147
262
|
const rows =
|
|
148
263
|
kind === undefined
|
|
149
|
-
? db.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_symbols WHERE address BETWEEN ? AND ? ORDER BY address`).all(range.start, range.end)
|
|
150
|
-
: db.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_symbols WHERE address BETWEEN ? AND ? AND kind = ? ORDER BY address`).all(range.start, range.end, kind);
|
|
151
|
-
return rows.map((row) => toSymbol(row as Raw));
|
|
264
|
+
? db.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_symbols WHERE space = ? AND address BETWEEN ? AND ? ORDER BY address`).all(space, range.start, range.end)
|
|
265
|
+
: db.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_symbols WHERE space = ? AND address BETWEEN ? AND ? AND kind = ? ORDER BY address`).all(space, range.start, range.end, kind);
|
|
266
|
+
return rows.map((row) => toSymbol(row as Raw)).filter(kept(origins));
|
|
152
267
|
}
|
|
153
268
|
|
|
154
|
-
export function listRegions(db: DatabaseSync | undefined, range: Range = ALL, type?: RegionType): RegionRow[] {
|
|
155
|
-
return regionsOverlapping(db, range.start, range.end).filter((region) => type === undefined || region.type === type);
|
|
269
|
+
export function listRegions(db: DatabaseSync | undefined, range: Range = ALL, type?: RegionType, origins?: OriginFilter, space: Space = DEFAULT_SPACE): RegionRow[] {
|
|
270
|
+
return regionsOverlapping(db, range.start, range.end, space).filter((region) => (type === undefined || region.type === type) && kept(origins)(region));
|
|
156
271
|
}
|
|
157
272
|
|
|
158
|
-
export function listComments(db: DatabaseSync | undefined, range: Range = ALL): CommentRow[] {
|
|
273
|
+
export function listComments(db: DatabaseSync | undefined, range: Range = ALL, origins?: OriginFilter, space: Space = DEFAULT_SPACE): CommentRow[] {
|
|
159
274
|
if (db === undefined) return [];
|
|
160
275
|
return db
|
|
161
|
-
.prepare(`SELECT ${COMMENT_COLUMNS} FROM current_comments WHERE address BETWEEN ? AND ? ORDER BY address, placement`)
|
|
162
|
-
.all(range.start, range.end)
|
|
163
|
-
.map((row) => toComment(row as Raw))
|
|
276
|
+
.prepare(`SELECT ${COMMENT_COLUMNS} FROM current_comments WHERE space = ? AND address BETWEEN ? AND ? ORDER BY address, placement`)
|
|
277
|
+
.all(space, range.start, range.end)
|
|
278
|
+
.map((row) => toComment(row as Raw))
|
|
279
|
+
.filter(kept(origins));
|
|
164
280
|
}
|
|
165
281
|
|
|
166
282
|
/** Current references whose source or target lies in the range. */
|
|
167
|
-
export function listReferences(db: DatabaseSync | undefined, range: Range = ALL, kind?: ReferenceKind): ReferenceRow[] {
|
|
283
|
+
export function listReferences(db: DatabaseSync | undefined, range: Range = ALL, kind?: ReferenceKind, origins?: OriginFilter, space: Space = DEFAULT_SPACE): ReferenceRow[] {
|
|
168
284
|
if (db === undefined) return [];
|
|
169
285
|
return db
|
|
170
286
|
.prepare(
|
|
171
|
-
`SELECT ${REFERENCE_COLUMNS} FROM current_references WHERE (from_address BETWEEN ? AND ? OR to_address BETWEEN ? AND ?) ORDER BY from_address, to_address, kind`,
|
|
287
|
+
`SELECT ${REFERENCE_COLUMNS} FROM current_references WHERE space = ? AND (from_address BETWEEN ? AND ? OR to_address BETWEEN ? AND ?) ORDER BY from_address, to_address, kind`,
|
|
172
288
|
)
|
|
173
|
-
.all(range.start, range.end, range.start, range.end)
|
|
289
|
+
.all(space, range.start, range.end, range.start, range.end)
|
|
174
290
|
.map((row) => toReference(row as Raw))
|
|
175
|
-
.filter((reference) => kind === undefined || reference.kind === kind);
|
|
291
|
+
.filter((reference) => (kind === undefined || reference.kind === kind) && kept(origins)(reference));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Current suppressions whose place lies in the range: a reference by its source or target. */
|
|
295
|
+
export function listSuppressions(db: DatabaseSync | undefined, range: Range = ALL, space: Space = DEFAULT_SPACE): SuppressionRow[] {
|
|
296
|
+
if (db === undefined) return [];
|
|
297
|
+
return db
|
|
298
|
+
.prepare(
|
|
299
|
+
`SELECT ${SUPPRESSION_COLUMNS} FROM current_suppressions WHERE space = ? AND ((start_address <= ? AND end_address >= ?) OR to_address BETWEEN ? AND ?) ORDER BY start_address, entity, to_address, kind`,
|
|
300
|
+
)
|
|
301
|
+
.all(space, range.end, range.start, range.start, range.end)
|
|
302
|
+
.map((row) => toSuppression(row as Raw));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Current modules that start in the range, in address order. */
|
|
306
|
+
export function listModules(db: DatabaseSync | undefined, range: Range = ALL, origins?: OriginFilter, space: Space = DEFAULT_SPACE): ModuleRow[] {
|
|
307
|
+
if (db === undefined) return [];
|
|
308
|
+
return db
|
|
309
|
+
.prepare(`SELECT ${MODULE_COLUMNS} FROM current_modules WHERE space = ? AND start_address BETWEEN ? AND ? ORDER BY start_address`)
|
|
310
|
+
.all(space, range.start, range.end)
|
|
311
|
+
.map((row) => toModule(row as Raw))
|
|
312
|
+
.filter(kept(origins));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** The spaces that hold current knowledge, the C64 first. */
|
|
316
|
+
export function listSpaces(db: DatabaseSync | undefined): Space[] {
|
|
317
|
+
if (db === undefined) return [];
|
|
318
|
+
const rows = db
|
|
319
|
+
.prepare(
|
|
320
|
+
`SELECT space FROM current_symbols UNION SELECT space FROM current_regions UNION SELECT space FROM current_comments UNION SELECT space FROM current_references UNION SELECT space FROM current_suppressions UNION SELECT space FROM current_modules`,
|
|
321
|
+
)
|
|
322
|
+
.all() as Array<{ space: Space }>;
|
|
323
|
+
const order = (space: Space) => (space === "c64" ? -2 : space === "drive8" ? -1 : Number(space.slice(4)));
|
|
324
|
+
return rows.map((row) => row.space).sort((a, b) => order(a) - order(b));
|
|
176
325
|
}
|
|
177
326
|
|
|
178
|
-
/** Case-insensitive text search in current symbol names and comment texts. */
|
|
179
|
-
export function search(db: DatabaseSync | undefined, text: string, limit = 50): { symbols: SymbolRow[]; comments: CommentRow[] } {
|
|
327
|
+
/** Case-insensitive text search in current symbol names and comment texts of one space. */
|
|
328
|
+
export function search(db: DatabaseSync | undefined, text: string, limit = 50, origins?: OriginFilter, space: Space = DEFAULT_SPACE): { symbols: SymbolRow[]; comments: CommentRow[] } {
|
|
180
329
|
if (db === undefined || text.length === 0) return { symbols: [], comments: [] };
|
|
181
330
|
const pattern = `%${text.replace(/[\\%_]/g, (character) => `\\${character}`)}%`;
|
|
331
|
+
const only = origins === undefined ? "" : ` AND origin IN (${origins.map(() => "?").join(", ")})`;
|
|
332
|
+
const values = origins ?? [];
|
|
182
333
|
const symbols = db
|
|
183
|
-
.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_symbols WHERE name LIKE ? ESCAPE '\\' ORDER BY address LIMIT ?`)
|
|
184
|
-
.all(pattern, limit)
|
|
334
|
+
.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_symbols WHERE space = ? AND name LIKE ? ESCAPE '\\'${only} ORDER BY address LIMIT ?`)
|
|
335
|
+
.all(space, pattern, ...values, limit)
|
|
185
336
|
.map((row) => toSymbol(row as Raw));
|
|
186
337
|
const comments = db
|
|
187
|
-
.prepare(`SELECT ${COMMENT_COLUMNS} FROM current_comments WHERE text LIKE ? ESCAPE '\\' ORDER BY address, placement LIMIT ?`)
|
|
188
|
-
.all(pattern, limit)
|
|
338
|
+
.prepare(`SELECT ${COMMENT_COLUMNS} FROM current_comments WHERE space = ? AND text LIKE ? ESCAPE '\\'${only} ORDER BY address, placement LIMIT ?`)
|
|
339
|
+
.all(space, pattern, ...values, limit)
|
|
189
340
|
.map((row) => toComment(row as Raw));
|
|
190
341
|
return { symbols, comments };
|
|
191
342
|
}
|
package/src/knowledge/schema.ts
CHANGED
|
@@ -5,20 +5,60 @@ import type { DatabaseSync } from "node:sqlite";
|
|
|
5
5
|
import { REFERENCE_KINDS, type ReferenceKind } from "../c64.ts";
|
|
6
6
|
import { KnowledgeError } from "./database.ts";
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/** heuristic: 6502 idioms that the static analysis finds by pattern (D44); an analyzer origin. */
|
|
9
|
+
export const ORIGINS = ["user", "llm", "dxa", "ghidra", "heuristic"] as const;
|
|
9
10
|
export type Origin = (typeof ORIGINS)[number];
|
|
10
11
|
export const SYMBOL_KINDS = ["label", "routine", "variable", "data", "vector"] as const;
|
|
11
12
|
export type SymbolKind = (typeof SYMBOL_KINDS)[number];
|
|
12
|
-
export const REGION_TYPES = [
|
|
13
|
+
export const REGION_TYPES = [
|
|
14
|
+
"code",
|
|
15
|
+
"bytes",
|
|
16
|
+
"words",
|
|
17
|
+
"addresses",
|
|
18
|
+
"addresses-split",
|
|
19
|
+
"rts-addresses",
|
|
20
|
+
"rts-addresses-split",
|
|
21
|
+
"jumps",
|
|
22
|
+
"petscii",
|
|
23
|
+
"screen",
|
|
24
|
+
"bitmap",
|
|
25
|
+
"sprite",
|
|
26
|
+
"workspace",
|
|
27
|
+
"unused",
|
|
28
|
+
"dual-use",
|
|
29
|
+
] as const;
|
|
13
30
|
export type RegionType = (typeof REGION_TYPES)[number];
|
|
31
|
+
/** Region types that are code: a JMP table is code too. dual-use is neither code nor data (D42). */
|
|
32
|
+
export const CODE_REGION_TYPES: readonly RegionType[] = ["code", "jumps"];
|
|
33
|
+
/** Region types that take no element size: their elements are instructions. */
|
|
34
|
+
export const NO_ELEMENT_TYPES: readonly RegionType[] = ["code", "jumps"];
|
|
35
|
+
export const MAX_ELEMENT = 256;
|
|
36
|
+
/** What a revision can name as its evidence (D42): a coverage run, a test, any project file, or a note. */
|
|
37
|
+
export const EVIDENCE_KINDS = ["coverage", "test", "file", "note"] as const;
|
|
38
|
+
export type EvidenceKind = (typeof EVIDENCE_KINDS)[number];
|
|
14
39
|
export const COMMENT_PLACEMENTS = ["line", "side"] as const;
|
|
15
40
|
export type CommentPlacement = (typeof COMMENT_PLACEMENTS)[number];
|
|
16
41
|
export { REFERENCE_KINDS, type ReferenceKind };
|
|
42
|
+
/** What a suppression blocks: a symbol address, a region range or one reference (D36). */
|
|
43
|
+
export const SUPPRESSION_ENTITIES = ["symbol", "region", "reference"] as const;
|
|
44
|
+
export type SuppressionEntity = (typeof SUPPRESSION_ENTITIES)[number];
|
|
45
|
+
|
|
46
|
+
/** A module file of the generated source (D48): a plain name that ends in .asm, no directory. */
|
|
47
|
+
export const MODULE_FILE = /^[A-Za-z0-9_][A-Za-z0-9_-]{0,59}\.asm$/;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The address space of a knowledge row (D53): the C64, the 1541 drive, or a
|
|
51
|
+
* cartridge bank. Drive code and banks reuse C64 addresses.
|
|
52
|
+
*/
|
|
53
|
+
export const SPACE = /^(c64|drive8|bank(0|[1-9][0-9]?|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$/;
|
|
54
|
+
export type Space = string;
|
|
55
|
+
export const DEFAULT_SPACE: Space = "c64";
|
|
56
|
+
const SPACE_CHECK = "space IN ('c64', 'drive8') OR space GLOB 'bank[0-9]' OR space GLOB 'bank[1-9][0-9]' OR space GLOB 'bank[12][0-9][0-9]'";
|
|
17
57
|
|
|
18
58
|
const list = (values: readonly string[]) => values.map((value) => `'${value}'`).join(", ");
|
|
19
59
|
|
|
20
60
|
/** The schema version this build reads and writes. */
|
|
21
|
-
export const SCHEMA_VERSION =
|
|
61
|
+
export const SCHEMA_VERSION = 6;
|
|
22
62
|
|
|
23
63
|
/**
|
|
24
64
|
* Creates the schema in an empty database. Knowledge rows are temporal: a
|
|
@@ -42,6 +82,7 @@ function createSchema(db: DatabaseSync): void {
|
|
|
42
82
|
);
|
|
43
83
|
CREATE TABLE symbols (
|
|
44
84
|
id INTEGER PRIMARY KEY,
|
|
85
|
+
space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
|
|
45
86
|
address INTEGER NOT NULL CHECK (address BETWEEN 0 AND 65535),
|
|
46
87
|
name TEXT NOT NULL CHECK (length(name) > 0),
|
|
47
88
|
kind TEXT NOT NULL CHECK (kind IN (${list(SYMBOL_KINDS)})),
|
|
@@ -49,21 +90,32 @@ function createSchema(db: DatabaseSync): void {
|
|
|
49
90
|
valid_from_revision INTEGER NOT NULL REFERENCES revisions (id),
|
|
50
91
|
valid_to_revision INTEGER REFERENCES revisions (id)
|
|
51
92
|
);
|
|
52
|
-
CREATE UNIQUE INDEX symbols_current_address ON symbols (address) WHERE valid_to_revision IS NULL;
|
|
53
|
-
CREATE UNIQUE INDEX symbols_current_name ON symbols (name) WHERE valid_to_revision IS NULL;
|
|
54
|
-
CREATE INDEX symbols_address ON symbols (address);
|
|
93
|
+
CREATE UNIQUE INDEX symbols_current_address ON symbols (space, address) WHERE valid_to_revision IS NULL;
|
|
94
|
+
CREATE UNIQUE INDEX symbols_current_name ON symbols (space, name) WHERE valid_to_revision IS NULL;
|
|
95
|
+
CREATE INDEX symbols_address ON symbols (space, address);
|
|
55
96
|
CREATE TABLE regions (
|
|
56
97
|
id INTEGER PRIMARY KEY,
|
|
98
|
+
space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
|
|
57
99
|
start_address INTEGER NOT NULL CHECK (start_address BETWEEN 0 AND 65535),
|
|
58
100
|
end_address INTEGER NOT NULL CHECK (end_address BETWEEN start_address AND 65535),
|
|
59
101
|
type TEXT NOT NULL CHECK (type IN (${list(REGION_TYPES)})),
|
|
102
|
+
element INTEGER CHECK (element BETWEEN 1 AND ${MAX_ELEMENT}),
|
|
60
103
|
origin TEXT NOT NULL CHECK (origin IN (${list(ORIGINS)})),
|
|
61
104
|
valid_from_revision INTEGER NOT NULL REFERENCES revisions (id),
|
|
62
105
|
valid_to_revision INTEGER REFERENCES revisions (id)
|
|
63
106
|
);
|
|
64
|
-
CREATE INDEX regions_start ON regions (start_address);
|
|
107
|
+
CREATE INDEX regions_start ON regions (space, start_address);
|
|
108
|
+
CREATE TABLE evidence (
|
|
109
|
+
id INTEGER PRIMARY KEY,
|
|
110
|
+
revision INTEGER NOT NULL REFERENCES revisions (id),
|
|
111
|
+
kind TEXT NOT NULL CHECK (kind IN (${list(EVIDENCE_KINDS)})),
|
|
112
|
+
ref TEXT NOT NULL CHECK (length(ref) > 0),
|
|
113
|
+
sha256 TEXT CHECK (sha256 IS NULL OR length(sha256) = 64)
|
|
114
|
+
);
|
|
115
|
+
CREATE INDEX evidence_revision ON evidence (revision);
|
|
65
116
|
CREATE TABLE comments (
|
|
66
117
|
id INTEGER PRIMARY KEY,
|
|
118
|
+
space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
|
|
67
119
|
address INTEGER NOT NULL CHECK (address BETWEEN 0 AND 65535),
|
|
68
120
|
placement TEXT NOT NULL CHECK (placement IN (${list(COMMENT_PLACEMENTS)})),
|
|
69
121
|
text TEXT NOT NULL CHECK (length(text) > 0),
|
|
@@ -71,10 +123,11 @@ function createSchema(db: DatabaseSync): void {
|
|
|
71
123
|
valid_from_revision INTEGER NOT NULL REFERENCES revisions (id),
|
|
72
124
|
valid_to_revision INTEGER REFERENCES revisions (id)
|
|
73
125
|
);
|
|
74
|
-
CREATE UNIQUE INDEX comments_current ON comments (address, placement) WHERE valid_to_revision IS NULL;
|
|
75
|
-
CREATE INDEX comments_address ON comments (address);
|
|
126
|
+
CREATE UNIQUE INDEX comments_current ON comments (space, address, placement) WHERE valid_to_revision IS NULL;
|
|
127
|
+
CREATE INDEX comments_address ON comments (space, address);
|
|
76
128
|
CREATE TABLE "references" (
|
|
77
129
|
id INTEGER PRIMARY KEY,
|
|
130
|
+
space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
|
|
78
131
|
from_address INTEGER NOT NULL CHECK (from_address BETWEEN 0 AND 65535),
|
|
79
132
|
to_address INTEGER NOT NULL CHECK (to_address BETWEEN 0 AND 65535),
|
|
80
133
|
kind TEXT NOT NULL CHECK (kind IN (${list(REFERENCE_KINDS)})),
|
|
@@ -82,13 +135,42 @@ function createSchema(db: DatabaseSync): void {
|
|
|
82
135
|
valid_from_revision INTEGER NOT NULL REFERENCES revisions (id),
|
|
83
136
|
valid_to_revision INTEGER REFERENCES revisions (id)
|
|
84
137
|
);
|
|
85
|
-
CREATE UNIQUE INDEX references_current ON "references" (from_address, to_address, kind) WHERE valid_to_revision IS NULL;
|
|
86
|
-
CREATE INDEX references_from ON "references" (from_address);
|
|
87
|
-
CREATE INDEX references_to ON "references" (to_address);
|
|
138
|
+
CREATE UNIQUE INDEX references_current ON "references" (space, from_address, to_address, kind) WHERE valid_to_revision IS NULL;
|
|
139
|
+
CREATE INDEX references_from ON "references" (space, from_address);
|
|
140
|
+
CREATE INDEX references_to ON "references" (space, to_address);
|
|
141
|
+
CREATE TABLE suppressions (
|
|
142
|
+
id INTEGER PRIMARY KEY,
|
|
143
|
+
space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
|
|
144
|
+
entity TEXT NOT NULL CHECK (entity IN (${list(SUPPRESSION_ENTITIES)})),
|
|
145
|
+
start_address INTEGER NOT NULL CHECK (start_address BETWEEN 0 AND 65535),
|
|
146
|
+
end_address INTEGER NOT NULL CHECK (end_address BETWEEN start_address AND 65535),
|
|
147
|
+
to_address INTEGER CHECK (to_address BETWEEN 0 AND 65535),
|
|
148
|
+
kind TEXT CHECK (kind IN (${list(REFERENCE_KINDS)})),
|
|
149
|
+
origin TEXT NOT NULL CHECK (origin IN ('user', 'llm')),
|
|
150
|
+
valid_from_revision INTEGER NOT NULL REFERENCES revisions (id),
|
|
151
|
+
valid_to_revision INTEGER REFERENCES revisions (id),
|
|
152
|
+
CHECK ((entity = 'reference') = (to_address IS NOT NULL AND kind IS NOT NULL)),
|
|
153
|
+
CHECK (entity = 'region' OR start_address = end_address)
|
|
154
|
+
);
|
|
155
|
+
CREATE INDEX suppressions_start ON suppressions (space, start_address);
|
|
156
|
+
CREATE TABLE modules (
|
|
157
|
+
id INTEGER PRIMARY KEY,
|
|
158
|
+
space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
|
|
159
|
+
start_address INTEGER NOT NULL CHECK (start_address BETWEEN 0 AND 65535),
|
|
160
|
+
file TEXT NOT NULL CHECK (length(file) > 0),
|
|
161
|
+
title TEXT CHECK (title IS NULL OR length(title) > 0),
|
|
162
|
+
origin TEXT NOT NULL CHECK (origin IN ('user', 'llm')),
|
|
163
|
+
valid_from_revision INTEGER NOT NULL REFERENCES revisions (id),
|
|
164
|
+
valid_to_revision INTEGER REFERENCES revisions (id)
|
|
165
|
+
);
|
|
166
|
+
CREATE UNIQUE INDEX modules_current_start ON modules (space, start_address) WHERE valid_to_revision IS NULL;
|
|
167
|
+
CREATE UNIQUE INDEX modules_current_file ON modules (space, file) WHERE valid_to_revision IS NULL;
|
|
88
168
|
CREATE VIEW current_symbols AS SELECT * FROM symbols WHERE valid_to_revision IS NULL;
|
|
89
169
|
CREATE VIEW current_regions AS SELECT * FROM regions WHERE valid_to_revision IS NULL;
|
|
90
170
|
CREATE VIEW current_comments AS SELECT * FROM comments WHERE valid_to_revision IS NULL;
|
|
91
171
|
CREATE VIEW current_references AS SELECT * FROM "references" WHERE valid_to_revision IS NULL;
|
|
172
|
+
CREATE VIEW current_suppressions AS SELECT * FROM suppressions WHERE valid_to_revision IS NULL;
|
|
173
|
+
CREATE VIEW current_modules AS SELECT * FROM modules WHERE valid_to_revision IS NULL;
|
|
92
174
|
INSERT INTO meta (key, value) VALUES ('current_revision', '0');
|
|
93
175
|
`);
|
|
94
176
|
}
|