@henols/c64-re-tools 2.0.0-rc.9 → 2.0.0

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 (117) hide show
  1. package/README.md +97 -116
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +22 -6
  4. package/skills/c64-assembler/scripts/assemble.ts +42 -14
  5. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  6. package/skills/c64-basic/SKILL.md +10 -1
  7. package/skills/c64-basic/scripts/basic.ts +3 -3
  8. package/skills/c64-disk/SKILL.md +13 -4
  9. package/skills/c64-disk/scripts/disk.ts +7 -7
  10. package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
  11. package/skills/c64-docs/SKILL.md +138 -0
  12. package/skills/c64-docs/references/page-types.md +141 -0
  13. package/skills/c64-docs/references/style.md +43 -0
  14. package/skills/c64-docs/scripts/generate.ts +109 -0
  15. package/skills/c64-docs/scripts/lib/links.ts +167 -0
  16. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  17. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  18. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  19. package/skills/c64-docs/scripts/link.ts +239 -0
  20. package/skills/c64-emulator/SKILL.md +16 -5
  21. package/skills/c64-knowledge/SKILL.md +140 -18
  22. package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
  23. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  24. package/skills/c64-memory-map/SKILL.md +11 -2
  25. package/skills/c64-memory-map/scripts/memmap.ts +2 -2
  26. package/skills/c64-provenance/SKILL.md +66 -11
  27. package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
  28. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  29. package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
  30. package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
  31. package/skills/c64-provenance/scripts/provenance.ts +242 -40
  32. package/skills/c64-reverse-engineering/SKILL.md +51 -26
  33. package/skills/c64-source/SKILL.md +76 -28
  34. package/skills/c64-source/scripts/generate.ts +425 -127
  35. package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
  36. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  37. package/skills/c64-static-analysis/SKILL.md +35 -18
  38. package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
  39. package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
  40. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
  41. package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
  42. package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
  43. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  44. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  45. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  46. package/skills/c64-testing/SKILL.md +36 -7
  47. package/skills/c64-testing/references/scenario-format.md +12 -8
  48. package/skills/c64-testing/scripts/checklist.ts +3 -3
  49. package/skills/c64-testing/scripts/diff.ts +167 -0
  50. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  51. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +45 -34
  52. package/skills/c64-testing/scripts/run.ts +73 -21
  53. package/skills/c64-testing/scripts/test.ts +5 -5
  54. package/skills/c64-unpacker/SKILL.md +23 -9
  55. package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
  56. package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
  57. package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
  58. package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
  59. package/skills/c64-unpacker/scripts/unpack.ts +41 -24
  60. package/src/c64.ts +103 -5
  61. package/src/cli/cleanup.ts +1 -1
  62. package/src/cli/main.ts +7 -4
  63. package/src/cli/status.ts +70 -3
  64. package/src/graphics.ts +158 -0
  65. package/src/host/main.ts +18 -4
  66. package/src/host/server.ts +10 -3
  67. package/src/host/tools/petcat.ts +1 -1
  68. package/src/host/tools/status.ts +1 -1
  69. package/src/host/vice/adapter.ts +4 -3
  70. package/src/host/vice/process.ts +2 -2
  71. package/src/host/vice/screen.ts +1 -42
  72. package/src/host/vice/session.ts +196 -23
  73. package/src/host-client/autostart.ts +1 -1
  74. package/src/host-client/connect.ts +1 -1
  75. package/src/host-client/coverage.ts +2 -29
  76. package/src/host-client/image.ts +66 -0
  77. package/src/host-client/steps.ts +9 -4
  78. package/src/host-client/tools.ts +1 -0
  79. package/src/host-client/transfer.ts +39 -11
  80. package/src/host-client/vice-session.ts +33 -16
  81. package/src/knowledge/conflicts.ts +161 -0
  82. package/src/knowledge/database.ts +150 -38
  83. package/src/knowledge/expression.ts +209 -0
  84. package/src/knowledge/graph.ts +104 -0
  85. package/src/knowledge/history.ts +70 -22
  86. package/src/knowledge/import.ts +64 -55
  87. package/src/knowledge/join.ts +211 -0
  88. package/src/knowledge/program.ts +266 -0
  89. package/src/knowledge/read.ts +258 -27
  90. package/src/knowledge/schema.ts +199 -75
  91. package/src/knowledge/text.ts +415 -0
  92. package/src/knowledge/write.ts +740 -57
  93. package/src/mcp/main.ts +1 -1
  94. package/src/mcp/output.ts +15 -8
  95. package/src/mcp/server.ts +3 -1
  96. package/src/mcp/tools/debug.ts +97 -16
  97. package/src/mcp/tools/execution.ts +38 -5
  98. package/src/mcp/tools/machine.ts +11 -2
  99. package/src/mcp/tools/media.ts +18 -13
  100. package/src/mcp/tools/memory.ts +17 -8
  101. package/src/mcp/tools/video.ts +9 -9
  102. package/src/native/discover.ts +2 -2
  103. package/src/native/ghidra/analyze.ts +2 -2
  104. package/src/native/ghidra/index.ts +1 -1
  105. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  106. package/src/native/ghidra/scripts/C64Export.java +1 -1
  107. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  108. package/src/native/types.ts +14 -8
  109. package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
  110. package/src/platform.ts +70 -7
  111. package/src/project.ts +180 -3
  112. package/src/protocol/messages.ts +1 -1
  113. package/src/protocol/tools.ts +2 -16
  114. package/src/protocol/vice.ts +124 -15
  115. package/src/script.ts +46 -7
  116. package/skills/c64-assembler/scripts/compare.ts +0 -84
  117. package/skills/c64-source/scripts/source.ts +0 -595
@@ -1,11 +1,13 @@
1
- // The knowledge.db schema: one version, created in an empty database.
1
+ // The schema of the knowledge index, .c64-re-tools/local/knowledge.db.
2
+ // The index is made from the text in .c64-re-tools/knowledge/; an index with
3
+ // another version is emptied and made again.
2
4
 
3
5
  import type { DatabaseSync } from "node:sqlite";
4
6
 
5
7
  import { REFERENCE_KINDS, type ReferenceKind } from "../c64.ts";
6
- import { KnowledgeError } from "./database.ts";
8
+ import { OPERAND_SELECTS } from "./expression.ts";
7
9
 
8
- /** heuristic: 6502 idioms that the static analysis finds by pattern (D44); an analyzer origin. */
10
+ /** heuristic: 6502 idioms that the static analysis finds by pattern; an analyzer origin. */
9
11
  export const ORIGINS = ["user", "llm", "dxa", "ghidra", "heuristic"] as const;
10
12
  export type Origin = (typeof ORIGINS)[number];
11
13
  export const SYMBOL_KINDS = ["label", "routine", "variable", "data", "vector"] as const;
@@ -23,30 +25,50 @@ export const REGION_TYPES = [
23
25
  "screen",
24
26
  "bitmap",
25
27
  "sprite",
28
+ "charset",
26
29
  "workspace",
27
30
  "unused",
28
31
  "dual-use",
29
32
  ] as const;
30
33
  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). */
34
+ /** Region types that are code: a JMP table is code too. dual-use is neither code nor data. */
32
35
  export const CODE_REGION_TYPES: readonly RegionType[] = ["code", "jumps"];
33
36
  /** Region types that take no element size: their elements are instructions. */
34
37
  export const NO_ELEMENT_TYPES: readonly RegionType[] = ["code", "jumps"];
35
38
  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. */
39
+ /** What a revision can name as its evidence: a coverage run, a test, any project file, or a note. */
37
40
  export const EVIDENCE_KINDS = ["coverage", "test", "file", "note"] as const;
38
41
  export type EvidenceKind = (typeof EVIDENCE_KINDS)[number];
39
42
  export const COMMENT_PLACEMENTS = ["line", "side"] as const;
40
43
  export type CommentPlacement = (typeof COMMENT_PLACEMENTS)[number];
41
44
  export { REFERENCE_KINDS, type ReferenceKind };
42
- /** What a suppression blocks: a symbol address, a region range or one reference (D36). */
45
+ /** The reference kind that only operand expressions write: code that builds the address of a symbol. */
46
+ export const POINTER_KIND: ReferenceKind = "pointer";
47
+ export { OPERAND_SELECTS, type OperandSelect } from "./expression.ts";
48
+ /** What a suppression blocks: a symbol address, a region range or one reference. */
43
49
  export const SUPPRESSION_ENTITIES = ["symbol", "region", "reference"] as const;
44
50
  export type SuppressionEntity = (typeof SUPPRESSION_ENTITIES)[number];
45
51
 
46
- /** A module file of the generated source (D48): a plain name that ends in .asm, no directory. */
52
+ /** A module file of the generated source: a plain name that ends in .asm, no directory. */
47
53
  export const MODULE_FILE = /^[A-Za-z0-9_][A-Za-z0-9_-]{0,59}\.asm$/;
54
+ /** A switch of the generated source: upper case, so it reads as a build option and not as a label. */
55
+ export const SWITCH_NAME = /^[A-Z][A-Z0-9_]{1,31}$/;
56
+ /** A macro name and a macro parameter. The body names a parameter with a dot in front: .dst. */
57
+ export const MACRO_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,31}$/;
58
+ export const MACRO_PARAM = /^[a-z_][a-z0-9_]{0,31}$/;
59
+
60
+ /**
61
+ * The kinds of a coverage range, one for each flag of a coverage map
62
+ * (COVERAGE in c64.ts), and wx: written and executed in the same run, so
63
+ * self-modified code still shows after the runs are joined.
64
+ */
65
+ export const COVERAGE_KINDS = ["r", "w", "x", "rom-r", "rom-x", "io-r", "io-w", "io-x", "wx"] as const;
66
+ export type CoverageKind = (typeof COVERAGE_KINDS)[number];
67
+ /** The most bytes of one memory row: a changed byte changes one short line of memory.jsonl. */
68
+ export const MEMORY_ROW = 64;
69
+
48
70
  /** 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;
71
+ export const GENERATED_FILES = ["main.asm", "loader.asm", "constants.asm", "macros.asm"] as const;
50
72
 
51
73
  /** True for a generated file name in any letter case: the file systems of macOS and Windows ignore case. */
52
74
  export function isGeneratedFile(file: string): boolean {
@@ -54,7 +76,7 @@ export function isGeneratedFile(file: string): boolean {
54
76
  }
55
77
 
56
78
  /**
57
- * The address space of a knowledge row (D53): the C64, the 1541 drive, or a
79
+ * The address space of a knowledge row: the C64, the 1541 drive, or a
58
80
  * cartridge bank. Drive code and banks reuse C64 addresses.
59
81
  */
60
82
  export const SPACE = /^(c64|drive8|bank(0|[1-9][0-9]?|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$/;
@@ -64,13 +86,13 @@ const SPACE_CHECK = "space IN ('c64', 'drive8') OR space GLOB 'bank[0-9]' OR spa
64
86
 
65
87
  const list = (values: readonly string[]) => values.map((value) => `'${value}'`).join(", ");
66
88
 
67
- /** The schema version this build reads and writes. */
68
- export const SCHEMA_VERSION = 6;
69
-
70
89
  /**
71
90
  * Creates the schema in an empty database. Knowledge rows are temporal: a
72
91
  * change closes the current row (valid_to_revision) and inserts a new one in
73
- * the same revision.
92
+ * the same revision. Revision and row ids are the text ids of the knowledge
93
+ * files; they sort in time order. The rules for current rows (one symbol at
94
+ * an address, ...) are not unique indexes, because a merged text can break
95
+ * them: the conflict check finds those rows (conflicts.ts).
74
96
  */
75
97
  function createSchema(db: DatabaseSync): void {
76
98
  db.exec(`
@@ -79,7 +101,7 @@ function createSchema(db: DatabaseSync): void {
79
101
  value TEXT NOT NULL
80
102
  );
81
103
  CREATE TABLE revisions (
82
- id INTEGER PRIMARY KEY,
104
+ id TEXT PRIMARY KEY,
83
105
  created_at TEXT NOT NULL,
84
106
  origin TEXT NOT NULL CHECK (origin IN (${list(ORIGINS)})),
85
107
  operation TEXT NOT NULL,
@@ -88,65 +110,78 @@ function createSchema(db: DatabaseSync): void {
88
110
  description TEXT
89
111
  );
90
112
  CREATE TABLE symbols (
91
- id INTEGER PRIMARY KEY,
113
+ id TEXT PRIMARY KEY,
92
114
  space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
93
115
  address INTEGER NOT NULL CHECK (address BETWEEN 0 AND 65535),
94
116
  name TEXT NOT NULL CHECK (length(name) > 0),
95
117
  kind TEXT NOT NULL CHECK (kind IN (${list(SYMBOL_KINDS)})),
96
118
  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)
119
+ valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
120
+ valid_to_revision TEXT REFERENCES revisions (id)
99
121
  );
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;
122
+ CREATE INDEX symbols_current_address ON symbols (space, address) WHERE valid_to_revision IS NULL;
123
+ CREATE INDEX symbols_current_name ON symbols (space, name) WHERE valid_to_revision IS NULL;
102
124
  CREATE INDEX symbols_address ON symbols (space, address);
125
+ CREATE TABLE analyzer_names (
126
+ id TEXT PRIMARY KEY,
127
+ space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
128
+ address INTEGER NOT NULL CHECK (address BETWEEN 0 AND 65535),
129
+ name TEXT NOT NULL CHECK (length(name) > 0),
130
+ kind TEXT NOT NULL CHECK (kind IN (${list(SYMBOL_KINDS)})),
131
+ origin TEXT NOT NULL CHECK (origin IN ('dxa', 'ghidra', 'heuristic')),
132
+ valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
133
+ valid_to_revision TEXT REFERENCES revisions (id)
134
+ );
135
+ CREATE INDEX analyzer_names_address ON analyzer_names (space, address);
136
+ CREATE INDEX analyzer_names_name ON analyzer_names (space, name);
103
137
  CREATE TABLE regions (
104
- id INTEGER PRIMARY KEY,
138
+ id TEXT PRIMARY KEY,
105
139
  space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
106
140
  start_address INTEGER NOT NULL CHECK (start_address BETWEEN 0 AND 65535),
107
141
  end_address INTEGER NOT NULL CHECK (end_address BETWEEN start_address AND 65535),
108
142
  type TEXT NOT NULL CHECK (type IN (${list(REGION_TYPES)})),
109
143
  element INTEGER CHECK (element BETWEEN 1 AND ${MAX_ELEMENT}),
110
144
  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)
145
+ valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
146
+ valid_to_revision TEXT REFERENCES revisions (id)
113
147
  );
114
148
  CREATE INDEX regions_start ON regions (space, start_address);
115
149
  CREATE TABLE evidence (
116
150
  id INTEGER PRIMARY KEY,
117
- revision INTEGER NOT NULL REFERENCES revisions (id),
151
+ revision TEXT NOT NULL REFERENCES revisions (id),
118
152
  kind TEXT NOT NULL CHECK (kind IN (${list(EVIDENCE_KINDS)})),
119
153
  ref TEXT NOT NULL CHECK (length(ref) > 0),
120
- sha256 TEXT CHECK (sha256 IS NULL OR length(sha256) = 64)
154
+ sha256 TEXT CHECK (sha256 IS NULL OR length(sha256) = 64),
155
+ space TEXT CHECK (space IS NULL OR ${SPACE_CHECK})
121
156
  );
122
157
  CREATE INDEX evidence_revision ON evidence (revision);
123
158
  CREATE TABLE comments (
124
- id INTEGER PRIMARY KEY,
159
+ id TEXT PRIMARY KEY,
125
160
  space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
126
161
  address INTEGER NOT NULL CHECK (address BETWEEN 0 AND 65535),
127
162
  placement TEXT NOT NULL CHECK (placement IN (${list(COMMENT_PLACEMENTS)})),
128
163
  text TEXT NOT NULL CHECK (length(text) > 0),
129
164
  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)
165
+ valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
166
+ valid_to_revision TEXT REFERENCES revisions (id)
132
167
  );
133
- CREATE UNIQUE INDEX comments_current ON comments (space, address, placement) WHERE valid_to_revision IS NULL;
168
+ CREATE INDEX comments_current ON comments (space, address, placement) WHERE valid_to_revision IS NULL;
134
169
  CREATE INDEX comments_address ON comments (space, address);
135
170
  CREATE TABLE "references" (
136
- id INTEGER PRIMARY KEY,
171
+ id TEXT PRIMARY KEY,
137
172
  space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
138
173
  from_address INTEGER NOT NULL CHECK (from_address BETWEEN 0 AND 65535),
139
174
  to_address INTEGER NOT NULL CHECK (to_address BETWEEN 0 AND 65535),
140
175
  kind TEXT NOT NULL CHECK (kind IN (${list(REFERENCE_KINDS)})),
141
176
  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)
177
+ valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
178
+ valid_to_revision TEXT REFERENCES revisions (id)
144
179
  );
145
- CREATE UNIQUE INDEX references_current ON "references" (space, from_address, to_address, kind) WHERE valid_to_revision IS NULL;
180
+ CREATE INDEX references_current ON "references" (space, from_address, to_address, kind) WHERE valid_to_revision IS NULL;
146
181
  CREATE INDEX references_from ON "references" (space, from_address);
147
182
  CREATE INDEX references_to ON "references" (space, to_address);
148
183
  CREATE TABLE suppressions (
149
- id INTEGER PRIMARY KEY,
184
+ id TEXT PRIMARY KEY,
150
185
  space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
151
186
  entity TEXT NOT NULL CHECK (entity IN (${list(SUPPRESSION_ENTITIES)})),
152
187
  start_address INTEGER NOT NULL CHECK (start_address BETWEEN 0 AND 65535),
@@ -154,76 +189,165 @@ function createSchema(db: DatabaseSync): void {
154
189
  to_address INTEGER CHECK (to_address BETWEEN 0 AND 65535),
155
190
  kind TEXT CHECK (kind IN (${list(REFERENCE_KINDS)})),
156
191
  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),
192
+ valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
193
+ valid_to_revision TEXT REFERENCES revisions (id),
159
194
  CHECK ((entity = 'reference') = (to_address IS NOT NULL AND kind IS NOT NULL)),
160
195
  CHECK (entity = 'region' OR start_address = end_address)
161
196
  );
162
197
  CREATE INDEX suppressions_start ON suppressions (space, start_address);
163
198
  CREATE TABLE modules (
164
- id INTEGER PRIMARY KEY,
199
+ id TEXT PRIMARY KEY,
165
200
  space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
166
201
  start_address INTEGER NOT NULL CHECK (start_address BETWEEN 0 AND 65535),
167
202
  file TEXT NOT NULL CHECK (length(file) > 0),
168
203
  title TEXT CHECK (title IS NULL OR length(title) > 0),
169
204
  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)
205
+ valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
206
+ valid_to_revision TEXT REFERENCES revisions (id)
207
+ );
208
+ CREATE INDEX modules_current_start ON modules (space, start_address) WHERE valid_to_revision IS NULL;
209
+ CREATE INDEX modules_current_file ON modules (space, file) WHERE valid_to_revision IS NULL;
210
+ CREATE TABLE operands (
211
+ id TEXT PRIMARY KEY,
212
+ space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
213
+ address INTEGER NOT NULL CHECK (address BETWEEN 0 AND 65535),
214
+ selector TEXT NOT NULL CHECK (selector IN (${list(OPERAND_SELECTS)})),
215
+ symbol TEXT CHECK (symbol IS NULL OR length(symbol) > 0),
216
+ offset INTEGER NOT NULL DEFAULT 0,
217
+ expression TEXT CHECK (expression IS NULL OR length(expression) > 0),
218
+ origin TEXT NOT NULL CHECK (origin IN ('user', 'llm')),
219
+ valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
220
+ valid_to_revision TEXT REFERENCES revisions (id),
221
+ CHECK ((selector = 'expr') = (expression IS NOT NULL AND symbol IS NULL AND offset = 0)),
222
+ CHECK (selector = 'expr' OR symbol IS NOT NULL)
223
+ );
224
+ CREATE INDEX operands_current ON operands (space, address) WHERE valid_to_revision IS NULL;
225
+ CREATE INDEX operands_address ON operands (space, address);
226
+ CREATE TABLE patches (
227
+ id TEXT PRIMARY KEY,
228
+ space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
229
+ start_address INTEGER NOT NULL CHECK (start_address BETWEEN 0 AND 65535),
230
+ end_address INTEGER NOT NULL CHECK (end_address BETWEEN start_address AND 65535),
231
+ switch_name TEXT NOT NULL CHECK (length(switch_name) > 0),
232
+ code TEXT NOT NULL,
233
+ comment TEXT CHECK (comment IS NULL OR length(comment) > 0),
234
+ origin TEXT NOT NULL CHECK (origin IN ('user', 'llm')),
235
+ valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
236
+ valid_to_revision TEXT REFERENCES revisions (id)
237
+ );
238
+ CREATE INDEX patches_start ON patches (space, start_address);
239
+ CREATE TABLE macros (
240
+ id TEXT PRIMARY KEY,
241
+ space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
242
+ name TEXT NOT NULL CHECK (length(name) > 0),
243
+ params TEXT NOT NULL,
244
+ body TEXT NOT NULL CHECK (length(body) > 0),
245
+ comment TEXT CHECK (comment IS NULL OR length(comment) > 0),
246
+ origin TEXT NOT NULL CHECK (origin IN ('user', 'llm')),
247
+ valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
248
+ valid_to_revision TEXT REFERENCES revisions (id)
249
+ );
250
+ CREATE INDEX macros_name ON macros (space, name);
251
+ CREATE TABLE macro_uses (
252
+ id TEXT PRIMARY KEY,
253
+ space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
254
+ start_address INTEGER NOT NULL CHECK (start_address BETWEEN 0 AND 65535),
255
+ end_address INTEGER NOT NULL CHECK (end_address BETWEEN start_address AND 65535),
256
+ macro TEXT NOT NULL CHECK (length(macro) > 0),
257
+ args TEXT NOT NULL,
258
+ origin TEXT NOT NULL CHECK (origin IN ('user', 'llm')),
259
+ valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
260
+ valid_to_revision TEXT REFERENCES revisions (id)
261
+ );
262
+ CREATE INDEX macro_uses_start ON macro_uses (space, start_address);
263
+ CREATE TABLE memory (
264
+ id TEXT PRIMARY KEY,
265
+ space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
266
+ address INTEGER NOT NULL CHECK (address BETWEEN 0 AND 65535),
267
+ bytes TEXT NOT NULL CHECK (length(bytes) BETWEEN 2 AND ${MEMORY_ROW * 2} AND length(bytes) % 2 = 0 AND address + length(bytes) / 2 <= 65536),
268
+ origin TEXT NOT NULL CHECK (origin IN ('user', 'llm')),
269
+ valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
270
+ valid_to_revision TEXT REFERENCES revisions (id)
271
+ );
272
+ CREATE INDEX memory_current ON memory (space, address) WHERE valid_to_revision IS NULL;
273
+ CREATE TABLE entries (
274
+ id TEXT PRIMARY KEY,
275
+ space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
276
+ address INTEGER NOT NULL CHECK (address BETWEEN 0 AND 65535),
277
+ state TEXT,
278
+ stages TEXT,
279
+ origin TEXT NOT NULL CHECK (origin IN ('user', 'llm')),
280
+ valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
281
+ valid_to_revision TEXT REFERENCES revisions (id)
172
282
  );
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;
283
+ CREATE INDEX entries_current ON entries (space) WHERE valid_to_revision IS NULL;
284
+ CREATE TABLE coverage (
285
+ id TEXT PRIMARY KEY,
286
+ space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
287
+ kind TEXT NOT NULL CHECK (kind IN (${list(COVERAGE_KINDS)})),
288
+ start_address INTEGER NOT NULL CHECK (start_address BETWEEN 0 AND 65535),
289
+ end_address INTEGER NOT NULL CHECK (end_address BETWEEN start_address AND 65535),
290
+ origin TEXT NOT NULL CHECK (origin IN ('user', 'llm')),
291
+ valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
292
+ valid_to_revision TEXT REFERENCES revisions (id)
293
+ );
294
+ CREATE INDEX coverage_current ON coverage (space, kind) WHERE valid_to_revision IS NULL;
295
+ CREATE VIEW current_memory AS SELECT * FROM memory WHERE valid_to_revision IS NULL;
296
+ CREATE VIEW current_entries AS SELECT * FROM entries WHERE valid_to_revision IS NULL;
297
+ CREATE VIEW current_coverage AS SELECT * FROM coverage WHERE valid_to_revision IS NULL;
175
298
  CREATE VIEW current_symbols AS SELECT * FROM symbols WHERE valid_to_revision IS NULL;
299
+ CREATE VIEW current_analyzer_names AS SELECT * FROM analyzer_names WHERE valid_to_revision IS NULL;
176
300
  CREATE VIEW current_regions AS SELECT * FROM regions WHERE valid_to_revision IS NULL;
177
301
  CREATE VIEW current_comments AS SELECT * FROM comments WHERE valid_to_revision IS NULL;
178
302
  CREATE VIEW current_references AS SELECT * FROM "references" WHERE valid_to_revision IS NULL;
179
303
  CREATE VIEW current_suppressions AS SELECT * FROM suppressions WHERE valid_to_revision IS NULL;
180
304
  CREATE VIEW current_modules AS SELECT * FROM modules WHERE valid_to_revision IS NULL;
181
- INSERT INTO meta (key, value) VALUES ('current_revision', '0');
305
+ CREATE VIEW current_operands AS SELECT * FROM operands WHERE valid_to_revision IS NULL;
306
+ CREATE VIEW current_patches AS SELECT * FROM patches WHERE valid_to_revision IS NULL;
307
+ CREATE VIEW current_macros AS SELECT * FROM macros WHERE valid_to_revision IS NULL;
308
+ CREATE VIEW current_macro_uses AS SELECT * FROM macro_uses WHERE valid_to_revision IS NULL;
182
309
  `);
183
310
  }
184
311
 
185
- export function schemaVersion(db: DatabaseSync): number {
312
+ /** The version of the index this build makes. An index with another version is emptied and made again. */
313
+ export const INDEX_VERSION = 6;
314
+
315
+ function indexVersion(db: DatabaseSync): number | undefined {
186
316
  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);
317
+ if (hasMeta === undefined) return undefined;
318
+ const row = db.prepare("SELECT value FROM meta WHERE key = 'index_version'").get() as { value: unknown } | undefined;
319
+ return row === undefined ? undefined : Number(row.value);
197
320
  }
198
321
 
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
- }
322
+ /** Drops every table and view of the database; the indexes go with their tables. */
323
+ function dropAll(db: DatabaseSync): void {
324
+ const objects = db.prepare("SELECT type, name FROM sqlite_master WHERE name NOT LIKE 'sqlite_%' AND type IN ('view', 'table')").all() as Array<{ type: string; name: string }>;
325
+ for (const { name } of objects.filter((object) => object.type === "view")) db.exec(`DROP VIEW "${name}"`);
326
+ for (const { name } of objects.filter((object) => object.type === "table")) db.exec(`DROP TABLE "${name}"`);
207
327
  }
208
328
 
209
329
  /**
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.
330
+ * Makes the index schema of this build. An empty database gets it; a database
331
+ * with another index version, or one that is not an index, is emptied first.
332
+ * The version is read again under the write lock, so two processes that open
333
+ * a new index at the same time make the schema once.
213
334
  */
214
335
  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;
336
+ if (indexVersion(db) === INDEX_VERSION) return;
337
+ // PRAGMA foreign_keys has no effect inside a transaction: switch it before.
338
+ db.exec("PRAGMA foreign_keys = OFF");
339
+ db.exec("BEGIN IMMEDIATE");
340
+ try {
341
+ if (indexVersion(db) !== INDEX_VERSION) {
342
+ dropAll(db);
343
+ createSchema(db);
344
+ db.prepare("INSERT INTO meta (key, value) VALUES ('index_version', ?)").run(String(INDEX_VERSION));
226
345
  }
346
+ db.exec("COMMIT");
347
+ } catch (error) {
348
+ db.exec("ROLLBACK");
349
+ throw error;
350
+ } finally {
351
+ db.exec("PRAGMA foreign_keys = ON");
227
352
  }
228
- checkVersion(schemaVersion(db));
229
353
  }