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

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 (54) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +11 -2
  4. package/skills/c64-assembler/scripts/assemble.ts +4 -4
  5. package/skills/c64-basic/SKILL.md +8 -1
  6. package/skills/c64-basic/scripts/basic.ts +3 -3
  7. package/skills/c64-disk/SKILL.md +10 -3
  8. package/skills/c64-disk/scripts/disk.ts +4 -4
  9. package/skills/c64-emulator/SKILL.md +4 -0
  10. package/skills/c64-knowledge/SKILL.md +62 -12
  11. package/skills/c64-knowledge/scripts/knowledge.ts +352 -102
  12. package/skills/c64-memory-map/SKILL.md +7 -0
  13. package/skills/c64-memory-map/scripts/memmap.ts +1 -1
  14. package/skills/c64-provenance/SKILL.md +56 -3
  15. package/skills/c64-provenance/scripts/library.ts +584 -0
  16. package/skills/c64-provenance/scripts/provenance.ts +201 -9
  17. package/skills/c64-reverse-engineering/SKILL.md +16 -2
  18. package/skills/c64-source/SKILL.md +38 -7
  19. package/skills/c64-source/scripts/generate.ts +213 -29
  20. package/skills/c64-source/scripts/loader.ts +3 -1
  21. package/skills/c64-source/scripts/opcodes.ts +19 -0
  22. package/skills/c64-source/scripts/source.ts +387 -35
  23. package/skills/c64-static-analysis/SKILL.md +11 -2
  24. package/skills/c64-static-analysis/scripts/analyze.ts +95 -8
  25. package/skills/c64-static-analysis/scripts/macros.ts +233 -0
  26. package/skills/c64-testing/SKILL.md +8 -1
  27. package/skills/c64-testing/references/scenario-format.md +1 -1
  28. package/skills/c64-testing/scripts/checklist.ts +2 -2
  29. package/skills/c64-testing/scripts/run.ts +4 -4
  30. package/skills/c64-testing/scripts/scenario.ts +1 -1
  31. package/skills/c64-testing/scripts/test.ts +4 -4
  32. package/skills/c64-unpacker/SKILL.md +11 -4
  33. package/skills/c64-unpacker/scripts/unpack.ts +4 -4
  34. package/src/cli/main.ts +5 -3
  35. package/src/host-client/image.ts +1 -1
  36. package/src/host-client/transfer.ts +34 -10
  37. package/src/host-client/vice-session.ts +9 -9
  38. package/src/knowledge/conflicts.ts +157 -0
  39. package/src/knowledge/database.ts +150 -38
  40. package/src/knowledge/history.ts +54 -20
  41. package/src/knowledge/import.ts +3 -2
  42. package/src/knowledge/read.ts +169 -20
  43. package/src/knowledge/schema.ts +115 -72
  44. package/src/knowledge/text.ts +391 -0
  45. package/src/knowledge/write.ts +508 -51
  46. package/src/mcp/output.ts +14 -7
  47. package/src/mcp/tools/debug.ts +8 -6
  48. package/src/mcp/tools/media.ts +17 -13
  49. package/src/mcp/tools/memory.ts +9 -5
  50. package/src/mcp/tools/video.ts +6 -5
  51. package/src/platform.ts +28 -5
  52. package/src/project.ts +166 -3
  53. package/src/protocol/tools.ts +1 -1
  54. package/src/script.ts +46 -7
@@ -6,32 +6,43 @@ import type { DatabaseSync } from "node:sqlite";
6
6
  import { KnowledgeError } from "./database.ts";
7
7
  import {
8
8
  COMMENT_COLUMNS,
9
+ MACRO_COLUMNS,
10
+ MACRO_USE_COLUMNS,
9
11
  MODULE_COLUMNS,
10
12
  OPERAND_COLUMNS,
13
+ PATCH_COLUMNS,
11
14
  REFERENCE_COLUMNS,
12
15
  REGION_COLUMNS,
16
+ resolveRevision,
13
17
  SUPPRESSION_COLUMNS,
14
18
  SYMBOL_COLUMNS,
15
19
  toComment,
20
+ toMacro,
21
+ toMacroUse,
16
22
  toModule,
17
23
  toOperand,
24
+ toPatch,
18
25
  toReference,
19
26
  toRegion,
20
27
  toSuppression,
21
28
  toSymbol,
22
29
  type CommentRow,
30
+ type MacroRow,
31
+ type MacroUseRow,
23
32
  type ModuleRow,
24
33
  type OperandRow,
34
+ type PatchRow,
25
35
  type ReferenceRow,
26
36
  type RegionRow,
27
37
  type SuppressionRow,
28
38
  type SymbolRow,
29
39
  } from "./read.ts";
30
40
  import { DEFAULT_SPACE, type EvidenceKind, type Origin, type Space } from "./schema.ts";
41
+ import { compareText } from "./text.ts";
31
42
  import type { Evidence } from "./write.ts";
32
43
 
33
44
  export interface Revision {
34
- id: number;
45
+ id: string;
35
46
  createdAt: string;
36
47
  origin: Origin;
37
48
  operation: string;
@@ -44,19 +55,22 @@ export interface Revision {
44
55
 
45
56
  /** One knowledge row with its validity: from its first revision until the revision that closed it. */
46
57
  export type HistoryEntry =
47
- | { entity: "symbol"; row: SymbolRow; from: number; to?: number }
48
- | { entity: "region"; row: RegionRow; from: number; to?: number }
49
- | { entity: "comment"; row: CommentRow; from: number; to?: number }
50
- | { entity: "reference"; row: ReferenceRow; from: number; to?: number }
51
- | { entity: "suppression"; row: SuppressionRow; from: number; to?: number }
52
- | { entity: "module"; row: ModuleRow; from: number; to?: number }
53
- | { entity: "operand"; row: OperandRow; from: number; to?: number };
58
+ | { entity: "symbol"; row: SymbolRow; from: string; to?: string }
59
+ | { entity: "region"; row: RegionRow; from: string; to?: string }
60
+ | { entity: "comment"; row: CommentRow; from: string; to?: string }
61
+ | { entity: "reference"; row: ReferenceRow; from: string; to?: string }
62
+ | { entity: "suppression"; row: SuppressionRow; from: string; to?: string }
63
+ | { entity: "module"; row: ModuleRow; from: string; to?: string }
64
+ | { entity: "operand"; row: OperandRow; from: string; to?: string }
65
+ | { entity: "patch"; row: PatchRow; from: string; to?: string }
66
+ | { entity: "macro"; row: MacroRow; from: string; to?: string }
67
+ | { entity: "macro-use"; row: MacroUseRow; from: string; to?: string };
54
68
 
55
69
  type Raw = Record<string, unknown>;
56
70
 
57
71
  function toRevision(row: Raw): Revision {
58
72
  const revision: Revision = {
59
- id: row.id as number,
73
+ id: row.id as string,
60
74
  createdAt: row.created_at as string,
61
75
  origin: row.origin as Origin,
62
76
  operation: row.operation as string,
@@ -68,13 +82,14 @@ function toRevision(row: Raw): Revision {
68
82
  }
69
83
 
70
84
  function entry<E extends HistoryEntry["entity"]>(entity: E, row: unknown, raw: Raw): HistoryEntry {
71
- const result = { entity, row, from: raw.valid_from_revision as number } as HistoryEntry;
72
- if (raw.valid_to_revision !== null) result.to = raw.valid_to_revision as number;
85
+ const result = { entity, row, from: raw.valid_from_revision as string } as HistoryEntry;
86
+ if (raw.valid_to_revision !== null) result.to = raw.valid_to_revision as string;
73
87
  return result;
74
88
  }
75
89
 
90
+ /** Revision ids sort in time order as text (D69); a current row sorts after every closed one. */
76
91
  function bySequence(a: HistoryEntry, b: HistoryEntry): number {
77
- return a.from - b.from || (a.to ?? Infinity) - (b.to ?? Infinity);
92
+ return compareText(a.from, b.from) || compareText(a.to ?? "~", b.to ?? "~");
78
93
  }
79
94
 
80
95
  /** Every row, current or closed, that concerns one address of a space, oldest first. */
@@ -97,9 +112,23 @@ export function historyAt(db: DatabaseSync | undefined, address: number, space:
97
112
  }
98
113
  for (const raw of db.prepare(`SELECT ${MODULE_COLUMNS}${validity} FROM modules WHERE space = ? AND start_address = ?`).all(space, address) as Raw[]) entries.push(entry("module", toModule(raw), raw));
99
114
  for (const raw of db.prepare(`SELECT ${OPERAND_COLUMNS}${validity} FROM operands WHERE space = ? AND address = ?`).all(space, address) as Raw[]) entries.push(entry("operand", toOperand(raw), raw));
115
+ for (const raw of db.prepare(`SELECT ${PATCH_COLUMNS}${validity} FROM patches WHERE space = ? AND start_address <= ? AND end_address >= ?`).all(space, address, address) as Raw[]) {
116
+ entries.push(entry("patch", toPatch(raw), raw));
117
+ }
118
+ for (const raw of db.prepare(`SELECT ${MACRO_USE_COLUMNS}${validity} FROM macro_uses WHERE space = ? AND start_address <= ? AND end_address >= ?`).all(space, address, address) as Raw[]) {
119
+ entries.push(entry("macro-use", toMacroUse(raw), raw));
120
+ }
100
121
  return entries.sort(bySequence);
101
122
  }
102
123
 
124
+ /** Every row of a macro with this name, and every use of it, oldest first. */
125
+ export function historyOfMacro(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): HistoryEntry[] {
126
+ if (db === undefined) return [];
127
+ const macros = (db.prepare(`SELECT ${MACRO_COLUMNS}, valid_to_revision FROM macros WHERE space = ? AND name = ?`).all(space, name) as Raw[]).map((raw) => entry("macro", toMacro(raw), raw));
128
+ const uses = (db.prepare(`SELECT ${MACRO_USE_COLUMNS}, valid_to_revision FROM macro_uses WHERE space = ? AND macro = ?`).all(space, name) as Raw[]).map((raw) => entry("macro-use", toMacroUse(raw), raw));
129
+ return [...macros, ...uses].sort(bySequence);
130
+ }
131
+
103
132
  /** Every symbol row of a space that ever had this name, oldest first: a name can move between addresses. */
104
133
  export function historyNamed(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): HistoryEntry[] {
105
134
  if (db === undefined) return [];
@@ -109,7 +138,7 @@ export function historyNamed(db: DatabaseSync | undefined, name: string, space:
109
138
  }
110
139
 
111
140
  /** Why a revision was written and what it rests on: its description and its evidence, when it has them. */
112
- export function revisionNote(db: DatabaseSync | undefined, id: number): { reason?: string; evidence?: Evidence[] } {
141
+ export function revisionNote(db: DatabaseSync | undefined, id: string): { reason?: string; evidence?: Evidence[] } {
113
142
  const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
114
143
  if (db === undefined || raw === undefined) return {};
115
144
  const { description, evidence } = withEvidence(db, toRevision(raw));
@@ -125,13 +154,13 @@ function withEvidence(db: DatabaseSync, revision: Revision): Revision {
125
154
  }
126
155
 
127
156
  /** Revisions, newest first; with `before`, only revisions older than it. */
128
- export function revisions(db: DatabaseSync | undefined, options: { limit?: number; before?: number } = {}): Revision[] {
157
+ export function revisions(db: DatabaseSync | undefined, options: { limit?: number; before?: string } = {}): Revision[] {
129
158
  if (db === undefined) return [];
130
- return (
131
- db
132
- .prepare("SELECT * FROM revisions WHERE id < ? ORDER BY id DESC LIMIT ?")
133
- .all(options.before ?? Number.MAX_SAFE_INTEGER, options.limit ?? 50) as Raw[]
134
- ).map((row) => withEvidence(db, toRevision(row)));
159
+ const rows =
160
+ options.before === undefined
161
+ ? db.prepare("SELECT * FROM revisions ORDER BY id DESC LIMIT ?").all(options.limit ?? 50)
162
+ : db.prepare("SELECT * FROM revisions WHERE id < ? ORDER BY id DESC LIMIT ?").all(resolveRevision(db, options.before), options.limit ?? 50);
163
+ return (rows as Raw[]).map((row) => withEvidence(db, toRevision(row)));
135
164
  }
136
165
 
137
166
  export interface RevisionChanges {
@@ -143,7 +172,8 @@ export interface RevisionChanges {
143
172
  }
144
173
 
145
174
  /** One revision and exactly what it changed. */
146
- export function revision(db: DatabaseSync | undefined, id: number): RevisionChanges {
175
+ export function revision(db: DatabaseSync | undefined, text: string): RevisionChanges {
176
+ const id = resolveRevision(db, text);
147
177
  const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
148
178
  if (db === undefined || raw === undefined) throw new KnowledgeError("not-found", `There is no revision ${id}.`);
149
179
  const tables = [
@@ -154,6 +184,9 @@ export function revision(db: DatabaseSync | undefined, id: number): RevisionChan
154
184
  ["suppression", "suppressions", SUPPRESSION_COLUMNS, toSuppression],
155
185
  ["module", "modules", MODULE_COLUMNS, toModule],
156
186
  ["operand", "operands", OPERAND_COLUMNS, toOperand],
187
+ ["patch", "patches", PATCH_COLUMNS, toPatch],
188
+ ["macro", "macros", MACRO_COLUMNS, toMacro],
189
+ ["macro-use", "macro_uses", MACRO_USE_COLUMNS, toMacroUse],
157
190
  ] as const;
158
191
  const added: HistoryEntry[] = [];
159
192
  const closed: HistoryEntry[] = [];
@@ -167,3 +200,4 @@ export function revision(db: DatabaseSync | undefined, id: number): RevisionChan
167
200
  }
168
201
  return { revision: withEvidence(db, toRevision(raw)), added, closed };
169
202
  }
203
+
@@ -55,7 +55,7 @@ export interface NormalizedFindings {
55
55
 
56
56
  export interface ImportContext {
57
57
  /** The revision the caller read; the import is refused if knowledge changed since. */
58
- expectedRevision?: number;
58
+ expectedRevision?: string;
59
59
  description?: string;
60
60
  /** The SHA-256 of the analyzed image, as lowercase hex. */
61
61
  inputHash?: string;
@@ -92,7 +92,7 @@ export interface ImportCounts {
92
92
 
93
93
  export interface ImportResult {
94
94
  /** The new revision, or null when nothing changed. */
95
- revision: number | null;
95
+ revision: string | null;
96
96
  symbols: ImportCounts;
97
97
  regions: ImportCounts;
98
98
  references: ImportCounts;
@@ -386,6 +386,7 @@ export function importFindings(db: DatabaseSync, findings: NormalizedFindings, c
386
386
  importSymbols(db, change, findings, coverage, result);
387
387
  importRegions(db, change, findings, coverage, result);
388
388
  importReferences(db, change, findings, coverage, result);
389
+ change.check();
389
390
  result.revision = change.revision;
390
391
  return result;
391
392
  });
@@ -1,10 +1,12 @@
1
- // Current-state knowledge queries. A project without a database
1
+ // Current-state knowledge queries. A project without knowledge
2
2
  // reads as empty knowledge. Each query reads one address space (D53); the
3
3
  // default is the C64.
4
4
 
5
5
  import type { DatabaseSync } from "node:sqlite";
6
6
 
7
+ import { KnowledgeError } from "./database.ts";
7
8
  import { DEFAULT_SPACE, type CommentPlacement, type OperandSelect, type Origin, type ReferenceKind, type RegionType, type Space, type SuppressionEntity, type SymbolKind } from "./schema.ts";
9
+ import { NO_REVISION } from "./text.ts";
8
10
 
9
11
  export interface SymbolRow {
10
12
  space: Space;
@@ -12,7 +14,7 @@ export interface SymbolRow {
12
14
  name: string;
13
15
  kind: SymbolKind;
14
16
  origin: Origin;
15
- revision: number;
17
+ revision: string;
16
18
  }
17
19
 
18
20
  export interface RegionRow {
@@ -23,7 +25,7 @@ export interface RegionRow {
23
25
  /** Bytes per element of a table; the length is a multiple of it (D42). */
24
26
  element?: number;
25
27
  origin: Origin;
26
- revision: number;
28
+ revision: string;
27
29
  }
28
30
 
29
31
  export interface CommentRow {
@@ -32,7 +34,7 @@ export interface CommentRow {
32
34
  placement: CommentPlacement;
33
35
  text: string;
34
36
  origin: Origin;
35
- revision: number;
37
+ revision: string;
36
38
  }
37
39
 
38
40
  export interface ReferenceRow {
@@ -41,7 +43,7 @@ export interface ReferenceRow {
41
43
  to: number;
42
44
  kind: ReferenceKind;
43
45
  origin: Origin;
44
- revision: number;
46
+ revision: string;
45
47
  }
46
48
 
47
49
  /** A place that a user/LLM removal blocks for every analyzer (D36). `to` and `kind` are set for a reference only. */
@@ -53,7 +55,7 @@ export interface SuppressionRow {
53
55
  to?: number;
54
56
  kind?: ReferenceKind;
55
57
  origin: Extract<Origin, "user" | "llm">;
56
- revision: number;
58
+ revision: string;
57
59
  }
58
60
 
59
61
  /** Where a file of the generated source starts (D48): it runs to the next module or the end of its segment. */
@@ -63,7 +65,7 @@ export interface ModuleRow {
63
65
  file: string;
64
66
  title?: string;
65
67
  origin: Extract<Origin, "user" | "llm">;
66
- revision: number;
68
+ revision: string;
67
69
  }
68
70
 
69
71
  /**
@@ -79,9 +81,50 @@ export interface OperandRow {
79
81
  offset: number;
80
82
  expression?: string;
81
83
  origin: Extract<Origin, "user" | "llm">;
82
- revision: number;
84
+ revision: string;
83
85
  }
84
86
 
87
+ /**
88
+ * Code that replaces [start, end] when the build defines the switch as 1
89
+ * (D72). The code fits the range; NOP fills the rest. Empty code is only NOP.
90
+ */
91
+ export interface PatchRow {
92
+ space: Space;
93
+ start: number;
94
+ end: number;
95
+ switch: string;
96
+ code: string;
97
+ comment?: string;
98
+ origin: Extract<Origin, "user" | "llm">;
99
+ revision: string;
100
+ }
101
+
102
+ /** An ACME macro of the generated source (D73). The body names a parameter with a dot in front: .dst. */
103
+ export interface MacroRow {
104
+ space: Space;
105
+ name: string;
106
+ params: string[];
107
+ body: string;
108
+ comment?: string;
109
+ origin: Extract<Origin, "user" | "llm">;
110
+ revision: string;
111
+ }
112
+
113
+ /** The instructions in [start, end] written as one use of a macro, with one expression for each parameter (D73). */
114
+ export interface MacroUseRow {
115
+ space: Space;
116
+ start: number;
117
+ end: number;
118
+ macro: string;
119
+ args: string[];
120
+ origin: Extract<Origin, "user" | "llm">;
121
+ revision: string;
122
+ }
123
+
124
+ /** A list of params or args as the knowledge stores it: the items with ", " between them. */
125
+ export const listText = (items: readonly string[]) => items.join(", ");
126
+ const items = (text: string) => (text.trim() === "" ? [] : text.split(",").map((item) => item.trim()));
127
+
85
128
  type Raw = Record<string, unknown>;
86
129
 
87
130
  export const SYMBOL_COLUMNS = "space, address, name, kind, origin, valid_from_revision";
@@ -91,9 +134,12 @@ export const REFERENCE_COLUMNS = "space, from_address, to_address, kind, origin,
91
134
  export const SUPPRESSION_COLUMNS = "space, entity, start_address, end_address, to_address, kind, origin, valid_from_revision";
92
135
  export const MODULE_COLUMNS = "space, start_address, file, title, origin, valid_from_revision";
93
136
  export const OPERAND_COLUMNS = "space, address, selector, symbol, offset, expression, origin, valid_from_revision";
137
+ export const PATCH_COLUMNS = "space, start_address, end_address, switch_name, code, comment, origin, valid_from_revision";
138
+ export const MACRO_COLUMNS = "space, name, params, body, comment, origin, valid_from_revision";
139
+ export const MACRO_USE_COLUMNS = "space, start_address, end_address, macro, args, origin, valid_from_revision";
94
140
 
95
141
  export function toSymbol(row: Raw): SymbolRow {
96
- 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 };
142
+ 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 string };
97
143
  }
98
144
 
99
145
  export function toRegion(row: Raw): RegionRow {
@@ -103,7 +149,7 @@ export function toRegion(row: Raw): RegionRow {
103
149
  end: row.end_address as number,
104
150
  type: row.type as RegionType,
105
151
  origin: row.origin as Origin,
106
- revision: row.valid_from_revision as number,
152
+ revision: row.valid_from_revision as string,
107
153
  };
108
154
  if (row.element !== null && row.element !== undefined) region.element = row.element as number;
109
155
  return region;
@@ -116,12 +162,12 @@ export function toComment(row: Raw): CommentRow {
116
162
  placement: row.placement as CommentPlacement,
117
163
  text: row.text as string,
118
164
  origin: row.origin as Origin,
119
- revision: row.valid_from_revision as number,
165
+ revision: row.valid_from_revision as string,
120
166
  };
121
167
  }
122
168
 
123
169
  export function toReference(row: Raw): ReferenceRow {
124
- 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 };
170
+ 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 string };
125
171
  }
126
172
 
127
173
  export function toSuppression(row: Raw): SuppressionRow {
@@ -131,7 +177,7 @@ export function toSuppression(row: Raw): SuppressionRow {
131
177
  start: row.start_address as number,
132
178
  end: row.end_address as number,
133
179
  origin: row.origin as SuppressionRow["origin"],
134
- revision: row.valid_from_revision as number,
180
+ revision: row.valid_from_revision as string,
135
181
  };
136
182
  if (row.to_address !== null) suppression.to = row.to_address as number;
137
183
  if (row.kind !== null) suppression.kind = row.kind as ReferenceKind;
@@ -139,7 +185,7 @@ export function toSuppression(row: Raw): SuppressionRow {
139
185
  }
140
186
 
141
187
  export function toModule(row: Raw): ModuleRow {
142
- 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 };
188
+ 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 string };
143
189
  if (row.title !== null && row.title !== undefined) module.title = row.title as string;
144
190
  return module;
145
191
  }
@@ -151,16 +197,68 @@ export function toOperand(row: Raw): OperandRow {
151
197
  select: row.selector as OperandSelect,
152
198
  offset: row.offset as number,
153
199
  origin: row.origin as OperandRow["origin"],
154
- revision: row.valid_from_revision as number,
200
+ revision: row.valid_from_revision as string,
155
201
  };
156
202
  if (row.symbol !== null && row.symbol !== undefined) operand.symbol = row.symbol as string;
157
203
  if (row.expression !== null && row.expression !== undefined) operand.expression = row.expression as string;
158
204
  return operand;
159
205
  }
160
206
 
161
- export function currentRevision(db: DatabaseSync | undefined): number {
162
- if (db === undefined) return 0;
163
- return Number((db.prepare("SELECT value FROM meta WHERE key = 'current_revision'").get() as { value: string }).value);
207
+ export function toPatch(row: Raw): PatchRow {
208
+ const patch: PatchRow = {
209
+ space: row.space as Space,
210
+ start: row.start_address as number,
211
+ end: row.end_address as number,
212
+ switch: row.switch_name as string,
213
+ code: row.code as string,
214
+ origin: row.origin as PatchRow["origin"],
215
+ revision: row.valid_from_revision as string,
216
+ };
217
+ if (row.comment !== null && row.comment !== undefined) patch.comment = row.comment as string;
218
+ return patch;
219
+ }
220
+
221
+ export function toMacro(row: Raw): MacroRow {
222
+ const macro: MacroRow = { space: row.space as Space, name: row.name as string, params: items(row.params as string), body: row.body as string, origin: row.origin as MacroRow["origin"], revision: row.valid_from_revision as string };
223
+ if (row.comment !== null && row.comment !== undefined) macro.comment = row.comment as string;
224
+ return macro;
225
+ }
226
+
227
+ export function toMacroUse(row: Raw): MacroUseRow {
228
+ return {
229
+ space: row.space as Space,
230
+ start: row.start_address as number,
231
+ end: row.end_address as number,
232
+ macro: row.macro as string,
233
+ args: items(row.args as string),
234
+ origin: row.origin as MacroUseRow["origin"],
235
+ revision: row.valid_from_revision as string,
236
+ };
237
+ }
238
+
239
+ /** The id of the newest revision, or "0" when the knowledge has none (D69). */
240
+ export function currentRevision(db: DatabaseSync | undefined): string {
241
+ if (db === undefined) return "0";
242
+ return (db.prepare("SELECT max(id) AS id FROM revisions").get() as { id: string | null }).id ?? "0";
243
+ }
244
+
245
+ /** At most this many ids are named when a start of an id fits several revisions. */
246
+ const NAMED_REVISIONS = 5;
247
+
248
+ /**
249
+ * The revision that `text` names (D69): "0" (no revision), a full revision id,
250
+ * or a start of an id that only one revision has.
251
+ */
252
+ export function resolveRevision(db: DatabaseSync | undefined, text: string): string {
253
+ if (text === NO_REVISION) return NO_REVISION;
254
+ if (!/^[0-9a-zT.-]+$/.test(text)) throw new KnowledgeError("invalid-input", `${JSON.stringify(text)} is not a revision id. A revision id looks like 20261015T214501.123-k3f9qz; its start is enough when only one revision has it.`);
255
+ const found = (db?.prepare("SELECT id FROM revisions WHERE substr(id, 1, ?) = ? ORDER BY id LIMIT ?").all(text.length, text, NAMED_REVISIONS + 1) ?? []) as Array<{ id: string }>;
256
+ if (found.length === 0) throw new KnowledgeError("not-found", `There is no revision ${text}.`);
257
+ if (found.length > 1) {
258
+ const named = found.slice(0, NAMED_REVISIONS).map((row) => row.id).join(", ");
259
+ throw new KnowledgeError("invalid-input", `${text} is the start of more than one revision: ${named}${found.length > NAMED_REVISIONS ? ", ..." : ""}. Give more of the id.`);
260
+ }
261
+ return found[0]!.id;
164
262
  }
165
263
 
166
264
  export function symbolAt(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): SymbolRow | undefined {
@@ -249,6 +347,29 @@ export function operandAt(db: DatabaseSync | undefined, address: number, space:
249
347
  return row === undefined ? undefined : toOperand(row);
250
348
  }
251
349
 
350
+ /** Current patches that share at least one address with [start, end], in address order. */
351
+ export function patchesOverlapping(db: DatabaseSync | undefined, start: number, end: number, space: Space = DEFAULT_SPACE): PatchRow[] {
352
+ if (db === undefined) return [];
353
+ return db
354
+ .prepare(`SELECT ${PATCH_COLUMNS} FROM current_patches WHERE space = ? AND start_address <= ? AND end_address >= ? ORDER BY start_address`)
355
+ .all(space, end, start)
356
+ .map((row) => toPatch(row as Raw));
357
+ }
358
+
359
+ /** Current macro uses that share at least one address with [start, end], in address order. */
360
+ export function macroUsesOverlapping(db: DatabaseSync | undefined, start: number, end: number, space: Space = DEFAULT_SPACE): MacroUseRow[] {
361
+ if (db === undefined) return [];
362
+ return db
363
+ .prepare(`SELECT ${MACRO_USE_COLUMNS} FROM current_macro_uses WHERE space = ? AND start_address <= ? AND end_address >= ? ORDER BY start_address`)
364
+ .all(space, end, start)
365
+ .map((row) => toMacroUse(row as Raw));
366
+ }
367
+
368
+ export function macroNamed(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): MacroRow | undefined {
369
+ const row = db?.prepare(`SELECT ${MACRO_COLUMNS} FROM current_macros WHERE space = ? AND name = ?`).get(space, name) as Raw | undefined;
370
+ return row === undefined ? undefined : toMacro(row);
371
+ }
372
+
252
373
  /** Everything currently known at one address: the main lookup for skills. */
253
374
  export interface AddressKnowledge {
254
375
  space: Space;
@@ -261,9 +382,13 @@ export interface AddressKnowledge {
261
382
  referencesTo: ReferenceRow[];
262
383
  /** How the operand of the instruction here is written (D65). */
263
384
  operand?: OperandRow;
385
+ /** The patch whose range holds the address (D72). */
386
+ patch?: PatchRow;
387
+ /** The macro use whose range holds the address (D73). */
388
+ macroUse?: MacroUseRow;
264
389
  /** What user/LLM removals block here for the analyzers. */
265
390
  suppressions: SuppressionRow[];
266
- revision: number;
391
+ revision: string;
267
392
  }
268
393
 
269
394
  export function at(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): AddressKnowledge {
@@ -282,6 +407,10 @@ export function at(db: DatabaseSync | undefined, address: number, space: Space =
282
407
  if (region !== undefined) result.region = region;
283
408
  const operand = operandAt(db, address, space);
284
409
  if (operand !== undefined) result.operand = operand;
410
+ const [patch] = patchesOverlapping(db, address, address, space);
411
+ if (patch !== undefined) result.patch = patch;
412
+ const [macroUse] = macroUsesOverlapping(db, address, address, space);
413
+ if (macroUse !== undefined) result.macroUse = macroUse;
285
414
  return result;
286
415
  }
287
416
 
@@ -362,12 +491,32 @@ export function listOperands(db: DatabaseSync | undefined, range: Range = ALL, o
362
491
  .filter(kept(origins));
363
492
  }
364
493
 
494
+ /** Current patches that share an address with the range, of one switch or of all, in address order. */
495
+ export function listPatches(db: DatabaseSync | undefined, range: Range = ALL, switchName?: string, origins?: OriginFilter, space: Space = DEFAULT_SPACE): PatchRow[] {
496
+ return patchesOverlapping(db, range.start, range.end, space).filter((patch) => (switchName === undefined || patch.switch === switchName) && kept(origins)(patch));
497
+ }
498
+
499
+ /** Current macros, by name. */
500
+ export function listMacros(db: DatabaseSync | undefined, origins?: OriginFilter, space: Space = DEFAULT_SPACE): MacroRow[] {
501
+ if (db === undefined) return [];
502
+ return db
503
+ .prepare(`SELECT ${MACRO_COLUMNS} FROM current_macros WHERE space = ? ORDER BY name`)
504
+ .all(space)
505
+ .map((row) => toMacro(row as Raw))
506
+ .filter(kept(origins));
507
+ }
508
+
509
+ /** Current macro uses that share an address with the range, of one macro or of all, in address order. */
510
+ export function listMacroUses(db: DatabaseSync | undefined, range: Range = ALL, macro?: string, origins?: OriginFilter, space: Space = DEFAULT_SPACE): MacroUseRow[] {
511
+ return macroUsesOverlapping(db, range.start, range.end, space).filter((use) => (macro === undefined || use.macro === macro) && kept(origins)(use));
512
+ }
513
+
365
514
  /** The spaces that hold current knowledge, the C64 first. */
366
515
  export function listSpaces(db: DatabaseSync | undefined): Space[] {
367
516
  if (db === undefined) return [];
368
517
  const rows = db
369
518
  .prepare(
370
- `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 UNION SELECT space FROM current_operands`,
519
+ `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 UNION SELECT space FROM current_operands UNION SELECT space FROM current_patches UNION SELECT space FROM current_macros UNION SELECT space FROM current_macro_uses`,
371
520
  )
372
521
  .all() as Array<{ space: Space }>;
373
522
  const order = (space: Space) => (space === "c64" ? -2 : space === "drive8" ? -1 : Number(space.slice(4)));