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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -37
  3. package/distribution/package.ts +21 -0
  4. package/distribution/plugin.ts +122 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +76 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +110 -0
  8. package/skills/c64-assembler/scripts/compare.ts +84 -0
  9. package/skills/c64-basic/SKILL.md +66 -0
  10. package/skills/c64-basic/scripts/basic.ts +46 -0
  11. package/skills/c64-disk/SKILL.md +93 -0
  12. package/skills/c64-disk/scripts/containers.ts +124 -0
  13. package/skills/c64-disk/scripts/disk.ts +120 -0
  14. package/skills/c64-emulator/SKILL.md +134 -0
  15. package/skills/c64-knowledge/SKILL.md +166 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +492 -0
  17. package/skills/c64-memory-map/SKILL.md +79 -0
  18. package/skills/c64-memory-map/scripts/memmap.ts +92 -0
  19. package/skills/c64-provenance/SKILL.md +110 -0
  20. package/skills/c64-provenance/scripts/compare.ts +152 -0
  21. package/skills/c64-provenance/scripts/provenance.ts +334 -0
  22. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  23. package/skills/c64-provenance/scripts/separate.ts +140 -0
  24. package/skills/c64-reverse-engineering/SKILL.md +144 -0
  25. package/skills/c64-source/SKILL.md +126 -0
  26. package/skills/c64-source/scripts/generate.ts +490 -0
  27. package/skills/c64-source/scripts/loader.ts +571 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +595 -0
  30. package/skills/c64-static-analysis/SKILL.md +119 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +275 -0
  32. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  33. package/skills/c64-static-analysis/scripts/findings.ts +75 -0
  34. package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
  35. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  36. package/skills/c64-testing/SKILL.md +103 -0
  37. package/skills/c64-testing/references/scenario-format.md +116 -0
  38. package/skills/c64-testing/scripts/checklist.ts +51 -0
  39. package/skills/c64-testing/scripts/run.ts +189 -0
  40. package/skills/c64-testing/scripts/scenario.ts +493 -0
  41. package/skills/c64-testing/scripts/test.ts +65 -0
  42. package/skills/c64-unpacker/SKILL.md +135 -0
  43. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  44. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  45. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  46. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  47. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  48. package/src/c64.ts +411 -0
  49. package/src/cli/ap-sdk.ts +44 -0
  50. package/src/cli/cleanup.ts +194 -0
  51. package/src/cli/main.ts +125 -0
  52. package/src/cli/status.ts +61 -0
  53. package/src/host/main.ts +134 -0
  54. package/src/host/server.ts +392 -0
  55. package/src/host/tools/c1541.ts +284 -0
  56. package/src/host/tools/index.ts +35 -0
  57. package/src/host/tools/petcat.ts +77 -0
  58. package/src/host/tools/status.ts +47 -0
  59. package/src/host/tools/version.ts +41 -0
  60. package/src/host/vice/adapter.ts +804 -0
  61. package/src/host/vice/binary-monitor.ts +389 -0
  62. package/src/host/vice/chips.ts +118 -0
  63. package/src/host/vice/connect.ts +34 -0
  64. package/src/host/vice/process.ts +413 -0
  65. package/src/host/vice/screen.ts +184 -0
  66. package/src/host/vice/session.ts +1426 -0
  67. package/src/host/vice/text-monitor.ts +223 -0
  68. package/src/host-client/autostart.ts +55 -0
  69. package/src/host-client/connect.ts +241 -0
  70. package/src/host-client/coverage.ts +63 -0
  71. package/src/host-client/steps.ts +170 -0
  72. package/src/host-client/tools.ts +89 -0
  73. package/src/host-client/transfer.ts +126 -0
  74. package/src/host-client/vice-session.ts +316 -0
  75. package/src/knowledge/database.ts +132 -0
  76. package/src/knowledge/history.ts +163 -0
  77. package/src/knowledge/import.ts +391 -0
  78. package/src/knowledge/read.ts +342 -0
  79. package/src/knowledge/schema.ts +229 -0
  80. package/src/knowledge/write.ts +663 -0
  81. package/src/mcp/main.ts +134 -0
  82. package/src/mcp/output.ts +32 -0
  83. package/src/mcp/server.ts +316 -0
  84. package/src/mcp/tools/debug.ts +327 -0
  85. package/src/mcp/tools/execution.ts +117 -0
  86. package/src/mcp/tools/input.ts +72 -0
  87. package/src/mcp/tools/machine.ts +100 -0
  88. package/src/mcp/tools/media.ts +129 -0
  89. package/src/mcp/tools/memory.ts +250 -0
  90. package/src/mcp/tools/video.ts +271 -0
  91. package/src/native/acme.ts +116 -0
  92. package/src/native/discover.ts +179 -0
  93. package/src/native/dxa.ts +166 -0
  94. package/src/native/ghidra/analyze.ts +86 -0
  95. package/src/native/ghidra/index.ts +173 -0
  96. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  97. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  98. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  99. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  100. package/src/native/ghidra/scripts/C64Export.java +274 -0
  101. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  102. package/src/native/local.ts +34 -0
  103. package/src/native/processes.ts +350 -0
  104. package/src/native/run.ts +172 -0
  105. package/src/native/staging.ts +83 -0
  106. package/src/native/status.ts +113 -0
  107. package/src/native/types.ts +257 -0
  108. package/src/native/watchdog.ts +78 -0
  109. package/src/package.ts +21 -0
  110. package/src/platform.ts +441 -0
  111. package/src/project.ts +39 -0
  112. package/src/protocol/framing.ts +132 -0
  113. package/src/protocol/messages.ts +259 -0
  114. package/src/protocol/tools.ts +266 -0
  115. package/src/protocol/vice.ts +1348 -0
  116. package/src/script.ts +60 -0
  117. package/src/time.ts +15 -0
  118. package/src/trace.ts +194 -0
  119. package/THIRD-PARTY-NOTICES.md +0 -26
  120. package/bin/cli.mjs +0 -283
  121. package/skills/acme-build/SKILL.md +0 -260
  122. package/skills/acme-build/scripts/acme.mjs +0 -341
  123. package/skills/acme-build/template.a +0 -53
  124. package/skills/c64-disk-access/SKILL.md +0 -159
  125. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  126. package/skills/c64-memory-mapping/SKILL.md +0 -631
  127. package/skills/c64-memory-mapping/memmap.json +0 -8800
  128. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  129. package/skills/c64-petcat/SKILL.md +0 -87
  130. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  131. package/skills/c64-program-recon/SKILL.md +0 -706
  132. package/skills/c64-program-recon/references/control-flow.md +0 -180
  133. package/skills/c64-program-recon/references/graphics.md +0 -79
  134. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  135. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  136. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  137. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  138. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  139. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  140. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  141. package/skills/c64-provenance-diff/SKILL.md +0 -292
  142. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  143. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  144. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  145. package/skills/c64-ram-capture/SKILL.md +0 -474
  146. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  147. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  148. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  149. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  150. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  151. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  152. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  153. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  154. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  155. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  156. package/skills/c64-ram-capture/transients/README.md +0 -137
  157. package/skills/routine-queue-walker/SKILL.md +0 -394
  158. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  159. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -0,0 +1,342 @@
1
+ // Current-state knowledge queries. A project without a database
2
+ // reads as empty knowledge. Each query reads one address space (D53); the
3
+ // default is the C64.
4
+
5
+ import type { DatabaseSync } from "node:sqlite";
6
+
7
+ import { DEFAULT_SPACE, type CommentPlacement, type Origin, type ReferenceKind, type RegionType, type Space, type SuppressionEntity, type SymbolKind } from "./schema.ts";
8
+
9
+ export interface SymbolRow {
10
+ space: Space;
11
+ address: number;
12
+ name: string;
13
+ kind: SymbolKind;
14
+ origin: Origin;
15
+ revision: number;
16
+ }
17
+
18
+ export interface RegionRow {
19
+ space: Space;
20
+ start: number;
21
+ end: number;
22
+ type: RegionType;
23
+ /** Bytes per element of a table; the length is a multiple of it (D42). */
24
+ element?: number;
25
+ origin: Origin;
26
+ revision: number;
27
+ }
28
+
29
+ export interface CommentRow {
30
+ space: Space;
31
+ address: number;
32
+ placement: CommentPlacement;
33
+ text: string;
34
+ origin: Origin;
35
+ revision: number;
36
+ }
37
+
38
+ export interface ReferenceRow {
39
+ space: Space;
40
+ from: number;
41
+ to: number;
42
+ kind: ReferenceKind;
43
+ origin: Origin;
44
+ revision: number;
45
+ }
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
+
69
+ type Raw = Record<string, unknown>;
70
+
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";
77
+
78
+ export function toSymbol(row: Raw): SymbolRow {
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 };
80
+ }
81
+
82
+ export function toRegion(row: Raw): RegionRow {
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;
93
+ }
94
+
95
+ export function toComment(row: Raw): CommentRow {
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
+ };
104
+ }
105
+
106
+ export function toReference(row: Raw): ReferenceRow {
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;
128
+ }
129
+
130
+ export function currentRevision(db: DatabaseSync | undefined): number {
131
+ if (db === undefined) return 0;
132
+ return Number((db.prepare("SELECT value FROM meta WHERE key = 'current_revision'").get() as { value: string }).value);
133
+ }
134
+
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;
137
+ return row === undefined ? undefined : toSymbol(row);
138
+ }
139
+
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;
142
+ return row === undefined ? undefined : toSymbol(row);
143
+ }
144
+
145
+ /** Current regions that share at least one address with [start, end], in address order. */
146
+ export function regionsOverlapping(db: DatabaseSync | undefined, start: number, end: number, space: Space = DEFAULT_SPACE): RegionRow[] {
147
+ if (db === undefined) return [];
148
+ return db
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)
151
+ .map((row) => toRegion(row as Raw));
152
+ }
153
+
154
+ export function commentsAt(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): CommentRow[] {
155
+ if (db === undefined) return [];
156
+ return db
157
+ .prepare(`SELECT ${COMMENT_COLUMNS} FROM current_comments WHERE space = ? AND address = ? ORDER BY placement`)
158
+ .all(space, address)
159
+ .map((row) => toComment(row as Raw));
160
+ }
161
+
162
+ export function referencesFrom(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): ReferenceRow[] {
163
+ if (db === undefined) return [];
164
+ return db
165
+ .prepare(`SELECT ${REFERENCE_COLUMNS} FROM current_references WHERE space = ? AND from_address = ? ORDER BY to_address, kind`)
166
+ .all(space, address)
167
+ .map((row) => toReference(row as Raw));
168
+ }
169
+
170
+ export function referencesTo(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): ReferenceRow[] {
171
+ if (db === undefined) return [];
172
+ return db
173
+ .prepare(`SELECT ${REFERENCE_COLUMNS} FROM current_references WHERE space = ? AND to_address = ? ORDER BY from_address, kind`)
174
+ .all(space, address)
175
+ .map((row) => toReference(row as Raw));
176
+ }
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
+
216
+ /** Everything currently known at one address: the main lookup for skills. */
217
+ export interface AddressKnowledge {
218
+ space: Space;
219
+ address: number;
220
+ symbol?: SymbolRow;
221
+ /** The current region that contains the address. */
222
+ region?: RegionRow;
223
+ comments: CommentRow[];
224
+ referencesFrom: ReferenceRow[];
225
+ referencesTo: ReferenceRow[];
226
+ /** What user/LLM removals block here for the analyzers. */
227
+ suppressions: SuppressionRow[];
228
+ revision: number;
229
+ }
230
+
231
+ export function at(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): AddressKnowledge {
232
+ const result: AddressKnowledge = {
233
+ space,
234
+ 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),
239
+ revision: currentRevision(db),
240
+ };
241
+ const symbol = symbolAt(db, address, space);
242
+ if (symbol !== undefined) result.symbol = symbol;
243
+ const [region] = regionsOverlapping(db, address, address, space);
244
+ if (region !== undefined) result.region = region;
245
+ return result;
246
+ }
247
+
248
+ export interface Range {
249
+ start: number;
250
+ end: number;
251
+ }
252
+
253
+ const ALL: Range = { start: 0x0000, end: 0xffff };
254
+
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[] {
261
+ if (db === undefined) return [];
262
+ const rows =
263
+ kind === undefined
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));
267
+ }
268
+
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));
271
+ }
272
+
273
+ export function listComments(db: DatabaseSync | undefined, range: Range = ALL, origins?: OriginFilter, space: Space = DEFAULT_SPACE): CommentRow[] {
274
+ if (db === undefined) return [];
275
+ return db
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));
280
+ }
281
+
282
+ /** Current references whose source or target lies in the range. */
283
+ export function listReferences(db: DatabaseSync | undefined, range: Range = ALL, kind?: ReferenceKind, origins?: OriginFilter, space: Space = DEFAULT_SPACE): ReferenceRow[] {
284
+ if (db === undefined) return [];
285
+ return db
286
+ .prepare(
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`,
288
+ )
289
+ .all(space, range.start, range.end, range.start, range.end)
290
+ .map((row) => toReference(row as Raw))
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));
325
+ }
326
+
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[] } {
329
+ if (db === undefined || text.length === 0) return { symbols: [], comments: [] };
330
+ const pattern = `%${text.replace(/[\\%_]/g, (character) => `\\${character}`)}%`;
331
+ const only = origins === undefined ? "" : ` AND origin IN (${origins.map(() => "?").join(", ")})`;
332
+ const values = origins ?? [];
333
+ const symbols = db
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)
336
+ .map((row) => toSymbol(row as Raw));
337
+ const comments = db
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)
340
+ .map((row) => toComment(row as Raw));
341
+ return { symbols, comments };
342
+ }
@@ -0,0 +1,229 @@
1
+ // The knowledge.db schema: one version, created in an empty database.
2
+
3
+ import type { DatabaseSync } from "node:sqlite";
4
+
5
+ import { REFERENCE_KINDS, type ReferenceKind } from "../c64.ts";
6
+ import { KnowledgeError } from "./database.ts";
7
+
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;
10
+ export type Origin = (typeof ORIGINS)[number];
11
+ export const SYMBOL_KINDS = ["label", "routine", "variable", "data", "vector"] as const;
12
+ export type SymbolKind = (typeof SYMBOL_KINDS)[number];
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;
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];
39
+ export const COMMENT_PLACEMENTS = ["line", "side"] as const;
40
+ export type CommentPlacement = (typeof COMMENT_PLACEMENTS)[number];
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
+ /** Files that c64-source writes itself: no module can have these names. */
49
+ export const GENERATED_FILES = ["main.asm", "loader.asm", "constants.asm"] as const;
50
+
51
+ /** True for a generated file name in any letter case: the file systems of macOS and Windows ignore case. */
52
+ export function isGeneratedFile(file: string): boolean {
53
+ return (GENERATED_FILES as readonly string[]).includes(file.toLowerCase());
54
+ }
55
+
56
+ /**
57
+ * The address space of a knowledge row (D53): the C64, the 1541 drive, or a
58
+ * cartridge bank. Drive code and banks reuse C64 addresses.
59
+ */
60
+ export const SPACE = /^(c64|drive8|bank(0|[1-9][0-9]?|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$/;
61
+ export type Space = string;
62
+ export const DEFAULT_SPACE: Space = "c64";
63
+ 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]'";
64
+
65
+ const list = (values: readonly string[]) => values.map((value) => `'${value}'`).join(", ");
66
+
67
+ /** The schema version this build reads and writes. */
68
+ export const SCHEMA_VERSION = 6;
69
+
70
+ /**
71
+ * Creates the schema in an empty database. Knowledge rows are temporal: a
72
+ * change closes the current row (valid_to_revision) and inserts a new one in
73
+ * the same revision.
74
+ */
75
+ function createSchema(db: DatabaseSync): void {
76
+ db.exec(`
77
+ CREATE TABLE meta (
78
+ key TEXT PRIMARY KEY,
79
+ value TEXT NOT NULL
80
+ );
81
+ CREATE TABLE revisions (
82
+ id INTEGER PRIMARY KEY,
83
+ created_at TEXT NOT NULL,
84
+ origin TEXT NOT NULL CHECK (origin IN (${list(ORIGINS)})),
85
+ operation TEXT NOT NULL,
86
+ input_hash TEXT,
87
+ tool_version TEXT,
88
+ description TEXT
89
+ );
90
+ CREATE TABLE symbols (
91
+ id INTEGER PRIMARY KEY,
92
+ space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
93
+ address INTEGER NOT NULL CHECK (address BETWEEN 0 AND 65535),
94
+ name TEXT NOT NULL CHECK (length(name) > 0),
95
+ kind TEXT NOT NULL CHECK (kind IN (${list(SYMBOL_KINDS)})),
96
+ origin TEXT NOT NULL CHECK (origin IN (${list(ORIGINS)})),
97
+ valid_from_revision INTEGER NOT NULL REFERENCES revisions (id),
98
+ valid_to_revision INTEGER REFERENCES revisions (id)
99
+ );
100
+ CREATE UNIQUE INDEX symbols_current_address ON symbols (space, address) WHERE valid_to_revision IS NULL;
101
+ CREATE UNIQUE INDEX symbols_current_name ON symbols (space, name) WHERE valid_to_revision IS NULL;
102
+ CREATE INDEX symbols_address ON symbols (space, address);
103
+ CREATE TABLE regions (
104
+ id INTEGER PRIMARY KEY,
105
+ space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
106
+ start_address INTEGER NOT NULL CHECK (start_address BETWEEN 0 AND 65535),
107
+ end_address INTEGER NOT NULL CHECK (end_address BETWEEN start_address AND 65535),
108
+ type TEXT NOT NULL CHECK (type IN (${list(REGION_TYPES)})),
109
+ element INTEGER CHECK (element BETWEEN 1 AND ${MAX_ELEMENT}),
110
+ origin TEXT NOT NULL CHECK (origin IN (${list(ORIGINS)})),
111
+ valid_from_revision INTEGER NOT NULL REFERENCES revisions (id),
112
+ valid_to_revision INTEGER REFERENCES revisions (id)
113
+ );
114
+ CREATE INDEX regions_start ON regions (space, start_address);
115
+ CREATE TABLE evidence (
116
+ id INTEGER PRIMARY KEY,
117
+ revision INTEGER NOT NULL REFERENCES revisions (id),
118
+ kind TEXT NOT NULL CHECK (kind IN (${list(EVIDENCE_KINDS)})),
119
+ ref TEXT NOT NULL CHECK (length(ref) > 0),
120
+ sha256 TEXT CHECK (sha256 IS NULL OR length(sha256) = 64)
121
+ );
122
+ CREATE INDEX evidence_revision ON evidence (revision);
123
+ CREATE TABLE comments (
124
+ id INTEGER PRIMARY KEY,
125
+ space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
126
+ address INTEGER NOT NULL CHECK (address BETWEEN 0 AND 65535),
127
+ placement TEXT NOT NULL CHECK (placement IN (${list(COMMENT_PLACEMENTS)})),
128
+ text TEXT NOT NULL CHECK (length(text) > 0),
129
+ origin TEXT NOT NULL CHECK (origin IN (${list(ORIGINS)})),
130
+ valid_from_revision INTEGER NOT NULL REFERENCES revisions (id),
131
+ valid_to_revision INTEGER REFERENCES revisions (id)
132
+ );
133
+ CREATE UNIQUE INDEX comments_current ON comments (space, address, placement) WHERE valid_to_revision IS NULL;
134
+ CREATE INDEX comments_address ON comments (space, address);
135
+ CREATE TABLE "references" (
136
+ id INTEGER PRIMARY KEY,
137
+ space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
138
+ from_address INTEGER NOT NULL CHECK (from_address BETWEEN 0 AND 65535),
139
+ to_address INTEGER NOT NULL CHECK (to_address BETWEEN 0 AND 65535),
140
+ kind TEXT NOT NULL CHECK (kind IN (${list(REFERENCE_KINDS)})),
141
+ origin TEXT NOT NULL CHECK (origin IN (${list(ORIGINS)})),
142
+ valid_from_revision INTEGER NOT NULL REFERENCES revisions (id),
143
+ valid_to_revision INTEGER REFERENCES revisions (id)
144
+ );
145
+ CREATE UNIQUE INDEX references_current ON "references" (space, from_address, to_address, kind) WHERE valid_to_revision IS NULL;
146
+ CREATE INDEX references_from ON "references" (space, from_address);
147
+ CREATE INDEX references_to ON "references" (space, to_address);
148
+ CREATE TABLE suppressions (
149
+ id INTEGER PRIMARY KEY,
150
+ space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
151
+ entity TEXT NOT NULL CHECK (entity IN (${list(SUPPRESSION_ENTITIES)})),
152
+ start_address INTEGER NOT NULL CHECK (start_address BETWEEN 0 AND 65535),
153
+ end_address INTEGER NOT NULL CHECK (end_address BETWEEN start_address AND 65535),
154
+ to_address INTEGER CHECK (to_address BETWEEN 0 AND 65535),
155
+ kind TEXT CHECK (kind IN (${list(REFERENCE_KINDS)})),
156
+ origin TEXT NOT NULL CHECK (origin IN ('user', 'llm')),
157
+ valid_from_revision INTEGER NOT NULL REFERENCES revisions (id),
158
+ valid_to_revision INTEGER REFERENCES revisions (id),
159
+ CHECK ((entity = 'reference') = (to_address IS NOT NULL AND kind IS NOT NULL)),
160
+ CHECK (entity = 'region' OR start_address = end_address)
161
+ );
162
+ CREATE INDEX suppressions_start ON suppressions (space, start_address);
163
+ CREATE TABLE modules (
164
+ id INTEGER PRIMARY KEY,
165
+ space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
166
+ start_address INTEGER NOT NULL CHECK (start_address BETWEEN 0 AND 65535),
167
+ file TEXT NOT NULL CHECK (length(file) > 0),
168
+ title TEXT CHECK (title IS NULL OR length(title) > 0),
169
+ origin TEXT NOT NULL CHECK (origin IN ('user', 'llm')),
170
+ valid_from_revision INTEGER NOT NULL REFERENCES revisions (id),
171
+ valid_to_revision INTEGER REFERENCES revisions (id)
172
+ );
173
+ CREATE UNIQUE INDEX modules_current_start ON modules (space, start_address) WHERE valid_to_revision IS NULL;
174
+ CREATE UNIQUE INDEX modules_current_file ON modules (space, file) WHERE valid_to_revision IS NULL;
175
+ CREATE VIEW current_symbols AS SELECT * FROM symbols WHERE valid_to_revision IS NULL;
176
+ CREATE VIEW current_regions AS SELECT * FROM regions WHERE valid_to_revision IS NULL;
177
+ CREATE VIEW current_comments AS SELECT * FROM comments WHERE valid_to_revision IS NULL;
178
+ CREATE VIEW current_references AS SELECT * FROM "references" WHERE valid_to_revision IS NULL;
179
+ CREATE VIEW current_suppressions AS SELECT * FROM suppressions WHERE valid_to_revision IS NULL;
180
+ CREATE VIEW current_modules AS SELECT * FROM modules WHERE valid_to_revision IS NULL;
181
+ INSERT INTO meta (key, value) VALUES ('current_revision', '0');
182
+ `);
183
+ }
184
+
185
+ export function schemaVersion(db: DatabaseSync): number {
186
+ const hasMeta = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'meta'").get();
187
+ if (hasMeta === undefined) {
188
+ const tables = db.prepare("SELECT count(*) AS n FROM sqlite_master WHERE type = 'table'").get() as { n: number };
189
+ if (tables.n > 0) throw new KnowledgeError("invalid-database", "The knowledge database has tables but no meta table; it is not a c64-re-tools database.");
190
+ return 0;
191
+ }
192
+ const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get() as { value: unknown } | undefined;
193
+ if (row === undefined) throw new KnowledgeError("invalid-database", "The knowledge database has no schema version.");
194
+ const value = String(row.value);
195
+ if (!/^(0|[1-9][0-9]{0,8})$/.test(value)) throw new KnowledgeError("invalid-database", `The schema version of the knowledge database is not a whole number: ${JSON.stringify(value)}.`);
196
+ return Number(value);
197
+ }
198
+
199
+ /** Refuses a database with any schema version other than the one this build reads and writes. */
200
+ export function checkVersion(version: number): void {
201
+ if (version !== SCHEMA_VERSION) {
202
+ throw new KnowledgeError(
203
+ "unsupported-schema",
204
+ `The knowledge database has schema version ${version}. This c64-re-tools reads and writes only version ${SCHEMA_VERSION}. Use the c64-re-tools release that wrote it.`,
205
+ );
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Creates the schema in an empty database and refuses any other version. The
211
+ * version is read again under the write lock, so two processes that open a
212
+ * new database at the same time create the schema once.
213
+ */
214
+ export function ensureSchema(db: DatabaseSync): void {
215
+ if (schemaVersion(db) === 0) {
216
+ db.exec("BEGIN IMMEDIATE");
217
+ try {
218
+ if (schemaVersion(db) === 0) {
219
+ createSchema(db);
220
+ db.prepare("INSERT INTO meta (key, value) VALUES ('schema_version', ?)").run(String(SCHEMA_VERSION));
221
+ }
222
+ db.exec("COMMIT");
223
+ } catch (error) {
224
+ db.exec("ROLLBACK");
225
+ throw error;
226
+ }
227
+ }
228
+ checkVersion(schemaVersion(db));
229
+ }