@henols/c64-re-tools 2.0.1 → 2.1.0-rc.2

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 (104) hide show
  1. package/README.md +8 -4
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +24 -9
  4. package/skills/c64-assembler/scripts/assembler.ts +95 -0
  5. package/skills/c64-basic/SKILL.md +17 -9
  6. package/skills/c64-basic/scripts/basic.ts +26 -36
  7. package/skills/c64-disk/SKILL.md +17 -7
  8. package/skills/c64-disk/scripts/disk.ts +130 -104
  9. package/skills/c64-disk/scripts/lib/containers.ts +10 -12
  10. package/skills/c64-docs/SKILL.md +47 -28
  11. package/skills/c64-docs/references/style.md +6 -6
  12. package/skills/c64-docs/scripts/docs.ts +323 -0
  13. package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
  14. package/skills/c64-emulator/SKILL.md +11 -7
  15. package/skills/c64-knowledge/SKILL.md +34 -21
  16. package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
  17. package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
  18. package/skills/c64-memory-map/SKILL.md +18 -10
  19. package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
  20. package/skills/c64-provenance/SKILL.md +82 -26
  21. package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
  22. package/skills/c64-provenance/scripts/lib/leftovers.ts +442 -0
  23. package/skills/c64-provenance/scripts/lib/library.ts +15 -25
  24. package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
  25. package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
  26. package/skills/c64-provenance/scripts/provenance.ts +222 -252
  27. package/skills/c64-reverse-engineering/SKILL.md +18 -15
  28. package/skills/c64-source/SKILL.md +28 -16
  29. package/skills/c64-source/scripts/lib/loader.ts +52 -77
  30. package/skills/c64-source/scripts/lib/source.ts +33 -42
  31. package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
  32. package/skills/c64-static-analysis/SKILL.md +24 -13
  33. package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
  34. package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
  35. package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
  36. package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
  37. package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
  38. package/skills/c64-testing/SKILL.md +94 -19
  39. package/skills/c64-testing/references/recording-format.md +125 -0
  40. package/skills/c64-testing/references/scenario-format.md +24 -19
  41. package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
  42. package/skills/c64-testing/scripts/lib/diff.ts +5 -3
  43. package/skills/c64-testing/scripts/lib/play.ts +334 -0
  44. package/skills/c64-testing/scripts/lib/recording.ts +662 -0
  45. package/skills/c64-testing/scripts/lib/replay.ts +105 -0
  46. package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
  47. package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
  48. package/skills/c64-testing/scripts/testing.ts +189 -0
  49. package/skills/c64-unpacker/SKILL.md +42 -25
  50. package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
  51. package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
  52. package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
  53. package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
  54. package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
  55. package/src/c64.ts +35 -20
  56. package/src/cli/main.ts +2 -2
  57. package/src/errors.ts +38 -0
  58. package/src/graphics.ts +3 -1
  59. package/src/host/server.ts +3 -2
  60. package/src/host/vice/session.ts +4 -4
  61. package/src/host-client/entry-state.ts +106 -0
  62. package/src/host-client/image.ts +21 -10
  63. package/src/host-client/steps.ts +285 -108
  64. package/src/host-client/transfer.ts +21 -15
  65. package/src/host-client/vice-session.ts +1 -1
  66. package/src/knowledge/database.ts +3 -23
  67. package/src/knowledge/expression.ts +8 -11
  68. package/src/knowledge/history.ts +2 -2
  69. package/src/knowledge/import.ts +13 -14
  70. package/src/knowledge/names.ts +50 -0
  71. package/src/knowledge/program.ts +11 -10
  72. package/src/knowledge/read.ts +16 -21
  73. package/src/knowledge/schema.ts +9 -3
  74. package/src/knowledge/text.ts +5 -5
  75. package/src/knowledge/write.ts +93 -137
  76. package/src/mcp/inputs.ts +105 -0
  77. package/src/mcp/output.ts +2 -13
  78. package/src/mcp/server.ts +12 -60
  79. package/src/mcp/tools/debug.ts +17 -12
  80. package/src/mcp/tools/execution.ts +5 -32
  81. package/src/mcp/tools/input.ts +7 -32
  82. package/src/mcp/tools/media.ts +6 -5
  83. package/src/mcp/tools/memory.ts +11 -38
  84. package/src/mcp/tools/video.ts +6 -5
  85. package/src/native/c64run.ts +162 -0
  86. package/src/native/discover.ts +8 -0
  87. package/src/native/dxa.ts +7 -6
  88. package/src/native/ghidra/analyze.ts +2 -1
  89. package/src/native/ghidra/index.ts +2 -1
  90. package/src/native/processes.ts +4 -3
  91. package/src/native/status.ts +25 -4
  92. package/src/native/types.ts +3 -7
  93. package/src/opcodes.ts +13 -13
  94. package/src/platform.ts +9 -9
  95. package/src/project.ts +16 -26
  96. package/src/protocol/messages.ts +3 -21
  97. package/src/protocol/vice.ts +7 -7
  98. package/src/script.ts +491 -49
  99. package/skills/c64-assembler/scripts/assemble.ts +0 -138
  100. package/skills/c64-docs/scripts/generate.ts +0 -109
  101. package/skills/c64-docs/scripts/link.ts +0 -239
  102. package/skills/c64-memory-map/scripts/memmap.ts +0 -92
  103. package/skills/c64-testing/scripts/checklist.ts +0 -51
  104. package/skills/c64-testing/scripts/test.ts +0 -65
@@ -6,10 +6,7 @@
6
6
  // << >> >>>, unary < and >, &, XOR, |. A unary < or > takes everything of a
7
7
  // higher priority after it, so <a+1 is the low byte of a+1.
8
8
 
9
- /** An expression that does not parse, or that names a symbol with no value. */
10
- export class ExpressionError extends Error {
11
- override name = "ExpressionError";
12
- }
9
+ import { Refusal } from "../errors.ts";
13
10
 
14
11
  /** The selectors of an operand expression: a byte of a symbol, its value, or free expression text. */
15
12
  export const OPERAND_SELECTS = ["lo", "hi", "value", "expr"] as const;
@@ -54,7 +51,7 @@ function tokenize(text: string): Token[] {
54
51
  }
55
52
  const number: RegExpExecArray | null = operandNext ? /^(\$[0-9A-Fa-f]+|%[01]+|[0-9]+)/.exec(rest) : null;
56
53
  const match: RegExpExecArray | null = number ?? /^(>>>|<<|>>|XOR\b)/.exec(rest) ?? NAME.exec(rest) ?? /^[-+*/%&|!<>()]/.exec(rest);
57
- if (!match) throw new ExpressionError(`"${text}" has an unknown character at position ${at + 1}: "${rest[0]}".`);
54
+ if (!match) throw new Refusal("invalid-input", `"${text}" has an unknown character at position ${at + 1}: "${rest[0]}".`);
58
55
  const token: string = match[0];
59
56
  tokens.push({ text: token, at });
60
57
  at += token.length;
@@ -65,13 +62,13 @@ function tokenize(text: string): Token[] {
65
62
  }
66
63
 
67
64
  function parse(text: string): Node {
68
- if (text.trim().length === 0) throw new ExpressionError("An expression is empty.");
69
- if (text.length > MAX_TEXT) throw new ExpressionError(`An expression has at most ${MAX_TEXT} characters.`);
65
+ if (text.trim().length === 0) throw new Refusal("invalid-input", "An expression is empty.");
66
+ if (text.length > MAX_TEXT) throw new Refusal("invalid-input", `An expression has at most ${MAX_TEXT} characters.`);
70
67
  const tokens = tokenize(text);
71
68
  let index = 0;
72
69
  const peek = () => tokens[index]?.text;
73
70
  const fail = (what: string): never => {
74
- throw new ExpressionError(`"${text}" ${what}.`);
71
+ throw new Refusal("invalid-input", `"${text}" ${what}.`);
75
72
  };
76
73
 
77
74
  const operand = (): Node => {
@@ -122,7 +119,7 @@ function value(node: Node, resolve: (name: string) => number | undefined): numbe
122
119
  return node.value;
123
120
  case "name": {
124
121
  const found = resolve(node.name);
125
- if (found === undefined) throw new ExpressionError(`"${node.name}" is no symbol of the knowledge and no platform name.`);
122
+ if (found === undefined) throw new Refusal("invalid-input", `"${node.name}" is no symbol of the knowledge and no platform name.`);
126
123
  return found;
127
124
  }
128
125
  case "unary": {
@@ -139,7 +136,7 @@ function value(node: Node, resolve: (name: string) => number | undefined): numbe
139
136
  return left * right;
140
137
  case "/":
141
138
  case "%":
142
- if (right === 0) throw new ExpressionError("An expression divides by zero.");
139
+ if (right === 0) throw new Refusal("invalid-input", "An expression divides by zero.");
143
140
  return node.operator === "/" ? Math.trunc(left / right) : left % right;
144
141
  case "+":
145
142
  return left + right;
@@ -162,7 +159,7 @@ function value(node: Node, resolve: (name: string) => number | undefined): numbe
162
159
  }
163
160
  }
164
161
 
165
- /** Throws ExpressionError when the text is no expression of this syntax. */
162
+ /** Throws an invalid-input Refusal when the text is no expression of this syntax. */
166
163
  export function checkExpression(text: string): void {
167
164
  parse(text);
168
165
  }
@@ -3,7 +3,7 @@
3
3
 
4
4
  import type { DatabaseSync } from "node:sqlite";
5
5
 
6
- import { KnowledgeError } from "./database.ts";
6
+ import { Refusal } from "../errors.ts";
7
7
  import {
8
8
  COMMENT_COLUMNS,
9
9
  MACRO_COLUMNS,
@@ -182,7 +182,7 @@ export interface RevisionChanges {
182
182
  export function revision(db: DatabaseSync | undefined, text: string): RevisionChanges {
183
183
  const id = resolveRevision(db, text);
184
184
  const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
185
- if (db === undefined || raw === undefined) throw new KnowledgeError("not-found", `There is no revision ${id}.`);
185
+ if (db === undefined || raw === undefined) throw new Refusal("not-found", `There is no revision ${id}.`);
186
186
  const tables = [
187
187
  ["symbol", "symbols", SYMBOL_COLUMNS, toSymbol],
188
188
  ["analyzer-name", "analyzer_names", SYMBOL_COLUMNS, toSymbol],
@@ -13,7 +13,9 @@
13
13
  import { createHash } from "node:crypto";
14
14
  import type { DatabaseSync } from "node:sqlite";
15
15
 
16
- import { KnowledgeError, transaction } from "./database.ts";
16
+ import { type AddressRange, formatC64Address } from "../c64.ts";
17
+ import { Refusal } from "../errors.ts";
18
+ import { transaction } from "./database.ts";
17
19
  import {
18
20
  analyzerNamed,
19
21
  REFERENCE_COLUMNS,
@@ -28,13 +30,10 @@ import {
28
30
  type RegionRow,
29
31
  type SymbolRow,
30
32
  } from "./read.ts";
31
- import { CODE_REGION_TYPES, DEFAULT_SPACE, POINTER_KIND, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
32
- import { Change, checkAddress, checkSpace, hex, subtract, SYMBOL_NAME, type AddressRange, type Evidence } from "./write.ts";
33
+ import { ANALYZERS, CODE_REGION_TYPES, DEFAULT_SPACE, POINTER_KIND, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type Analyzer, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
34
+ import { Change, checkAddress, checkSpace, subtract, SYMBOL_NAME, type Evidence } from "./write.ts";
33
35
 
34
- export type { AddressRange };
35
36
 
36
- export const ANALYZERS = ["dxa", "ghidra", "heuristic"] as const satisfies readonly Origin[];
37
- export type Analyzer = (typeof ANALYZERS)[number];
38
37
 
39
38
  export interface NormalizedFindings {
40
39
  analyzer: Analyzer;
@@ -106,7 +105,7 @@ const isCode = (value: SymbolKind | RegionType) => value === "label" || value ==
106
105
  const counts = (): ImportCounts => ({ added: 0, changed: 0, retired: 0, unchanged: 0, suppressed: 0 });
107
106
 
108
107
  function invalid(message: string): never {
109
- throw new KnowledgeError("invalid-input", `The analyzer result is not usable: ${message} Nothing was imported.`);
108
+ throw new Refusal("invalid-input", `The analyzer result is not usable: ${message} Nothing was imported.`);
110
109
  }
111
110
 
112
111
  /** Sorted, merged coverage ranges. */
@@ -133,7 +132,7 @@ export function validateFindings(findings: NormalizedFindings): AddressRange[] {
133
132
  for (const range of findings.coverage) {
134
133
  checkAddress(range.start, "coverage start");
135
134
  checkAddress(range.end, "coverage end");
136
- if (range.end < range.start) invalid(`coverage ${hex(range.start)}-${hex(range.end)} ends before it starts.`);
135
+ if (range.end < range.start) invalid(`coverage ${formatC64Address(range.start)}-${formatC64Address(range.end)} ends before it starts.`);
137
136
  }
138
137
  const coverage = mergeCoverage(findings.coverage);
139
138
  const covering = (start: number, end: number) => coverage.some((range) => range.start <= start && end <= range.end);
@@ -142,10 +141,10 @@ export function validateFindings(findings: NormalizedFindings): AddressRange[] {
142
141
  const names = new Set<string>();
143
142
  for (const symbol of findings.symbols) {
144
143
  checkAddress(symbol.address);
145
- if (!covering(symbol.address, symbol.address)) invalid(`the symbol at ${hex(symbol.address)} is outside the coverage.`);
144
+ if (!covering(symbol.address, symbol.address)) invalid(`the symbol at ${formatC64Address(symbol.address)} is outside the coverage.`);
146
145
  if (!SYMBOL_NAME.test(symbol.name)) invalid(`the symbol name ${JSON.stringify(symbol.name)} is not valid.`);
147
146
  if (!(SYMBOL_KINDS as readonly string[]).includes(symbol.kind)) invalid(`the symbol kind ${symbol.kind} is not known.`);
148
- if (addresses.has(symbol.address)) invalid(`two symbols are at ${hex(symbol.address)}.`);
147
+ if (addresses.has(symbol.address)) invalid(`two symbols are at ${formatC64Address(symbol.address)}.`);
149
148
  if (names.has(symbol.name)) invalid(`the name ${symbol.name} is at two addresses.`);
150
149
  addresses.add(symbol.address);
151
150
  names.add(symbol.name);
@@ -154,16 +153,16 @@ export function validateFindings(findings: NormalizedFindings): AddressRange[] {
154
153
  regions.forEach((region, index) => {
155
154
  checkAddress(region.start, "region start");
156
155
  checkAddress(region.end, "region end");
157
- if (region.end < region.start) invalid(`the region ${hex(region.start)}-${hex(region.end)} ends before it starts.`);
158
- if (!covering(region.start, region.end)) invalid(`the region ${hex(region.start)}-${hex(region.end)} is not inside the coverage.`);
156
+ if (region.end < region.start) invalid(`the region ${formatC64Address(region.start)}-${formatC64Address(region.end)} ends before it starts.`);
157
+ if (!covering(region.start, region.end)) invalid(`the region ${formatC64Address(region.start)}-${formatC64Address(region.end)} is not inside the coverage.`);
159
158
  if (!(REGION_TYPES as readonly string[]).includes(region.type)) invalid(`the region type ${region.type} is not known.`);
160
159
  const previous = regions[index - 1];
161
- if (previous !== undefined && region.start <= previous.end) invalid(`the regions at ${hex(previous.start)} and ${hex(region.start)} overlap.`);
160
+ if (previous !== undefined && region.start <= previous.end) invalid(`the regions at ${formatC64Address(previous.start)} and ${formatC64Address(region.start)} overlap.`);
162
161
  });
163
162
  for (const reference of findings.references) {
164
163
  checkAddress(reference.from, "reference source");
165
164
  checkAddress(reference.to, "reference target");
166
- if (!covering(reference.from, reference.from)) invalid(`the reference from ${hex(reference.from)} is outside the coverage.`);
165
+ if (!covering(reference.from, reference.from)) invalid(`the reference from ${formatC64Address(reference.from)} is outside the coverage.`);
167
166
  if (!(REFERENCE_KINDS as readonly string[]).includes(reference.kind)) invalid(`the reference kind ${reference.kind} is not known.`);
168
167
  if (reference.kind === POINTER_KIND) invalid(`only an operand expression writes a pointer reference, not an analyzer.`);
169
168
  }
@@ -0,0 +1,50 @@
1
+ // Names as addresses: the one place where a name in a command line, a step
2
+ // or a scenario becomes an address. A symbol comes first. An analyzer name
3
+ // counts when no symbol has the name and the analyzers that give it give it
4
+ // to one address.
5
+
6
+ import type { DatabaseSync } from "node:sqlite";
7
+
8
+ import { c64AddressOf, formatC64Address } from "../c64.ts";
9
+ import { Refusal } from "../errors.ts";
10
+ import { openForRead } from "./database.ts";
11
+ import { analyzerNamed, listAnalyzerNames, listSymbols, symbolNamed } from "./read.ts";
12
+ import { DEFAULT_SPACE, type Space, type SymbolKind } from "./schema.ts";
13
+
14
+ /** The address of a name in a space. Refuses a name that no symbol and no analyzer has, and an analyzer name at two addresses. */
15
+ export function addressOfName(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): number {
16
+ const symbol = symbolNamed(db, name, space);
17
+ if (symbol !== undefined) return symbol.address;
18
+ const rows = analyzerNamed(db, name, space);
19
+ const addresses = [...new Set(rows.map((row) => row.address))];
20
+ if (addresses.length === 1) return addresses[0]!;
21
+ const where = space === DEFAULT_SPACE ? "" : ` in ${space}`;
22
+ if (addresses.length > 1) throw new Refusal("invalid-input", `the analyzers give ${name} to ${rows.map((row) => `${formatC64Address(row.address)} (${row.origin})`).join(" and ")}${where}. Give the address`);
23
+ throw new Refusal("not-found", `no symbol or analyzer name${where} is ${name}`);
24
+ }
25
+
26
+ /** Every name of a space with its address: the symbols, and each analyzer name that addressOfName accepts. With kinds, only names of those kinds. */
27
+ export function nameTable(db: DatabaseSync | undefined, space: Space = DEFAULT_SPACE, kinds?: readonly SymbolKind[]): Map<string, number> {
28
+ const fits = (row: { kind: SymbolKind }) => kinds === undefined || kinds.includes(row.kind);
29
+ const names = new Map<string, number>();
30
+ for (const symbol of listSymbols(db, undefined, undefined, undefined, space)) if (fits(symbol)) names.set(symbol.name, symbol.address);
31
+ const analyzer = new Map<string, Set<number>>();
32
+ for (const row of listAnalyzerNames(db, undefined, undefined, undefined, space)) if (fits(row)) analyzer.set(row.name, (analyzer.get(row.name) ?? new Set()).add(row.address));
33
+ for (const [name, addresses] of analyzer) if (!names.has(name) && addresses.size === 1) names.set(name, [...addresses][0]!);
34
+ return names;
35
+ }
36
+
37
+ /** addressOfName on the knowledge of the project: the name resolver of the scripts. */
38
+ export function knowledgeNames(name: string, space: string): number {
39
+ const db = openForRead();
40
+ try {
41
+ return addressOfName(db, name, space);
42
+ } finally {
43
+ db?.close();
44
+ }
45
+ }
46
+
47
+ /** An address of a step or a file: $ and 1 to 4 hex digits, else a name of the knowledge in the space. */
48
+ export function knowledgeAt(space: Space = DEFAULT_SPACE): (location: string) => number {
49
+ return (location) => c64AddressOf(location) ?? knowledgeNames(location, space);
50
+ }
@@ -6,10 +6,11 @@
6
6
  import { createHash } from "node:crypto";
7
7
  import type { DatabaseSync } from "node:sqlite";
8
8
 
9
- import { COVERAGE } from "../c64.ts";
10
- import { KnowledgeError } from "./database.ts";
9
+ import { Refusal } from "../errors.ts";
10
+ import { type AddressRange, COVERAGE, formatC64Address } from "../c64.ts";
11
+
11
12
  import { COVERAGE_KINDS, DEFAULT_SPACE, MEMORY_ROW, type CoverageKind, type Space } from "./schema.ts";
12
- import { checkSpace, hex, write, type AddressRange, type Evidence, type WriteContext } from "./write.ts";
13
+ import { checkSpace, write, type Evidence, type WriteContext } from "./write.ts";
13
14
 
14
15
  /** The 64 KiB of one space: offset n is address n. */
15
16
  const SIZE = 0x10000;
@@ -84,13 +85,13 @@ function memoryRows(segments: readonly AddressRange[], byteAt: (address: number)
84
85
  }
85
86
 
86
87
  function checkSegments(segments: readonly AddressRange[]): void {
87
- if (segments.length === 0) throw new KnowledgeError("invalid-input", "An image has at least one segment.");
88
+ if (segments.length === 0) throw new Refusal("invalid-input", "An image has at least one segment.");
88
89
  const sorted = [...segments].sort((a, b) => a.start - b.start);
89
90
  sorted.forEach((segment, index) => {
90
91
  if (!Number.isInteger(segment.start) || !Number.isInteger(segment.end) || segment.start < 0 || segment.end > 0xffff || segment.end < segment.start) {
91
- throw new KnowledgeError("invalid-input", `The segment ${hex(segment.start)}-${hex(segment.end)} is not a range from $0000 to $ffff.`);
92
+ throw new Refusal("invalid-input", `The segment ${formatC64Address(segment.start)}-${formatC64Address(segment.end)} is not a range from $0000 to $ffff.`);
92
93
  }
93
- if (index > 0 && segment.start <= sorted[index - 1]!.end) throw new KnowledgeError("invalid-input", `The segments at ${hex(sorted[index - 1]!.start)} and ${hex(segment.start)} overlap.`);
94
+ if (index > 0 && segment.start <= sorted[index - 1]!.end) throw new Refusal("invalid-input", `The segments at ${formatC64Address(sorted[index - 1]!.start)} and ${formatC64Address(segment.start)} overlap.`);
94
95
  });
95
96
  }
96
97
 
@@ -102,8 +103,8 @@ function checkSegments(segments: readonly AddressRange[]): void {
102
103
  export function setImage(db: DatabaseSync, context: WriteContext, request: ImageRequest): { revision: string | null; current: { space: Space; bytes: number; rowsAdded: number; rowsClosed: number; entry: boolean } } {
103
104
  checkSpace(request);
104
105
  checkSegments(request.segments);
105
- if (request.entry !== undefined && (!Number.isInteger(request.entry) || request.entry < 0 || request.entry > 0xffff)) throw new KnowledgeError("invalid-input", "The entry must be from $0000 to $ffff.");
106
- if (request.entry === undefined && (request.state !== undefined || request.stages !== undefined)) throw new KnowledgeError("invalid-input", "A machine state or stages need an entry.");
106
+ if (request.entry !== undefined && (!Number.isInteger(request.entry) || request.entry < 0 || request.entry > 0xffff)) throw new Refusal("invalid-input", "The entry must be from $0000 to $ffff.");
107
+ if (request.entry === undefined && (request.state !== undefined || request.stages !== undefined)) throw new Refusal("invalid-input", "A machine state or stages need an entry.");
107
108
  const space = request.space ?? DEFAULT_SPACE;
108
109
  const wanted = memoryRows(request.segments, request.byteAt);
109
110
  return write(db, context, "image", (change) => {
@@ -177,7 +178,7 @@ export function addCoverage(
177
178
  checkSpace(request);
178
179
  const space = request.space ?? DEFAULT_SPACE;
179
180
  for (const map of request.maps) {
180
- if (map.bytes.length !== SIZE) throw new KnowledgeError("invalid-input", `${map.name} has ${map.bytes.length} bytes; a coverage map has 65536, one for each address.`);
181
+ if (map.bytes.length !== SIZE) throw new Refusal("invalid-input", `${map.name} has ${map.bytes.length} bytes; a coverage map has 65536, one for each address.`);
181
182
  }
182
183
  const hashed = request.maps.map((map) => ({ ...map, sha256: createHash("sha256").update(map.bytes).digest("hex") }));
183
184
  const known = new Set(coverageRuns(db).filter((run) => run.space === space).map((run) => `${run.name}\n${run.sha256}`));
@@ -235,7 +236,7 @@ export function requireProgram(db: DatabaseSync | undefined, space: Space = DEFA
235
236
  if (image !== undefined) return image;
236
237
  const where = space === DEFAULT_SPACE ? "" : ` in ${space}`;
237
238
  const option = space === DEFAULT_SPACE ? "" : ` --space ${space}`;
238
- throw new KnowledgeError("not-found", `The knowledge has no program${where}. Put it in with knowledge.ts image <image.json|prg>${option} of the c64-knowledge skill.`);
239
+ throw new Refusal("not-found", `The knowledge has no program${where}. Put it in with knowledge.ts image <image.json|prg>${option} of the c64-knowledge skill.`);
239
240
  }
240
241
 
241
242
  /** The bytes for an analyzer: a PRG for one segment without an entry state, else the 64 KiB. */
@@ -4,7 +4,8 @@
4
4
 
5
5
  import type { DatabaseSync } from "node:sqlite";
6
6
 
7
- import { KnowledgeError } from "./database.ts";
7
+ import type { AddressRange } from "../c64.ts";
8
+ import { Refusal } from "../errors.ts";
8
9
  import { DEFAULT_SPACE, type CommentPlacement, type OperandSelect, type Origin, type ReferenceKind, type RegionType, type Space, type SuppressionEntity, type SymbolKind } from "./schema.ts";
9
10
  import { NO_REVISION } from "./text.ts";
10
11
 
@@ -251,12 +252,12 @@ const NAMED_REVISIONS = 5;
251
252
  */
252
253
  export function resolveRevision(db: DatabaseSync | undefined, text: string): string {
253
254
  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
+ if (!/^[0-9a-zT.-]+$/.test(text)) throw new Refusal("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
256
  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 === 0) throw new Refusal("not-found", `There is no revision ${text}.`);
257
258
  if (found.length > 1) {
258
259
  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
+ throw new Refusal("invalid-input", `${text} is the start of more than one revision: ${named}${found.length > NAMED_REVISIONS ? ", ..." : ""}. Give more of the id.`);
260
261
  }
261
262
  return found[0]!.id;
262
263
  }
@@ -288,7 +289,7 @@ export const ANALYZER_NAME_ORDER: readonly Origin[] = ["heuristic", "ghidra", "d
288
289
  const byAnalyzer = (a: SymbolRow, b: SymbolRow) => a.address - b.address || ANALYZER_NAME_ORDER.indexOf(a.origin) - ANALYZER_NAME_ORDER.indexOf(b.origin);
289
290
 
290
291
  /** Current analyzer names in a range, in address order and then heuristic, Ghidra, DXA. */
291
- export function listAnalyzerNames(db: DatabaseSync | undefined, range: Range = ALL, kind?: SymbolKind, origins?: OriginFilter, space: Space = DEFAULT_SPACE): SymbolRow[] {
292
+ export function listAnalyzerNames(db: DatabaseSync | undefined, range: AddressRange = ALL, kind?: SymbolKind, origins?: OriginFilter, space: Space = DEFAULT_SPACE): SymbolRow[] {
292
293
  if (db === undefined) return [];
293
294
  return (db.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_analyzer_names WHERE space = ? AND address BETWEEN ? AND ?`).all(space, range.start, range.end) as Raw[])
294
295
  .map(toSymbol)
@@ -442,19 +443,13 @@ export function at(db: DatabaseSync | undefined, address: number, space: Space =
442
443
  return result;
443
444
  }
444
445
 
445
- export interface Range {
446
- start: number;
447
- end: number;
448
- }
449
-
450
- const ALL: Range = { start: 0x0000, end: 0xffff };
446
+ const ALL: AddressRange = { start: 0x0000, end: 0xffff };
451
447
 
452
448
  /** Rows of these origins only; undefined keeps every origin. */
453
449
  export type OriginFilter = readonly Origin[] | undefined;
454
450
  const kept = (origins: OriginFilter) => (row: { origin: Origin }) => origins === undefined || origins.includes(row.origin);
455
451
 
456
-
457
- export function listSymbols(db: DatabaseSync | undefined, range: Range = ALL, kind?: SymbolKind, origins?: OriginFilter, space: Space = DEFAULT_SPACE): SymbolRow[] {
452
+ export function listSymbols(db: DatabaseSync | undefined, range: AddressRange = ALL, kind?: SymbolKind, origins?: OriginFilter, space: Space = DEFAULT_SPACE): SymbolRow[] {
458
453
  if (db === undefined) return [];
459
454
  const rows =
460
455
  kind === undefined
@@ -463,11 +458,11 @@ export function listSymbols(db: DatabaseSync | undefined, range: Range = ALL, ki
463
458
  return rows.map((row) => toSymbol(row as Raw)).filter(kept(origins));
464
459
  }
465
460
 
466
- export function listRegions(db: DatabaseSync | undefined, range: Range = ALL, type?: RegionType, origins?: OriginFilter, space: Space = DEFAULT_SPACE): RegionRow[] {
461
+ export function listRegions(db: DatabaseSync | undefined, range: AddressRange = ALL, type?: RegionType, origins?: OriginFilter, space: Space = DEFAULT_SPACE): RegionRow[] {
467
462
  return regionsOverlapping(db, range.start, range.end, space).filter((region) => (type === undefined || region.type === type) && kept(origins)(region));
468
463
  }
469
464
 
470
- export function listComments(db: DatabaseSync | undefined, range: Range = ALL, origins?: OriginFilter, space: Space = DEFAULT_SPACE): CommentRow[] {
465
+ export function listComments(db: DatabaseSync | undefined, range: AddressRange = ALL, origins?: OriginFilter, space: Space = DEFAULT_SPACE): CommentRow[] {
471
466
  if (db === undefined) return [];
472
467
  return db
473
468
  .prepare(`SELECT ${COMMENT_COLUMNS} FROM current_comments WHERE space = ? AND address BETWEEN ? AND ? ORDER BY address, placement`)
@@ -477,7 +472,7 @@ export function listComments(db: DatabaseSync | undefined, range: Range = ALL, o
477
472
  }
478
473
 
479
474
  /** Current references whose source or target lies in the range. */
480
- export function listReferences(db: DatabaseSync | undefined, range: Range = ALL, kind?: ReferenceKind, origins?: OriginFilter, space: Space = DEFAULT_SPACE): ReferenceRow[] {
475
+ export function listReferences(db: DatabaseSync | undefined, range: AddressRange = ALL, kind?: ReferenceKind, origins?: OriginFilter, space: Space = DEFAULT_SPACE): ReferenceRow[] {
481
476
  if (db === undefined) return [];
482
477
  return db
483
478
  .prepare(
@@ -489,7 +484,7 @@ export function listReferences(db: DatabaseSync | undefined, range: Range = ALL,
489
484
  }
490
485
 
491
486
  /** Current suppressions whose place lies in the range: a reference by its source or target. */
492
- export function listSuppressions(db: DatabaseSync | undefined, range: Range = ALL, space: Space = DEFAULT_SPACE): SuppressionRow[] {
487
+ export function listSuppressions(db: DatabaseSync | undefined, range: AddressRange = ALL, space: Space = DEFAULT_SPACE): SuppressionRow[] {
493
488
  if (db === undefined) return [];
494
489
  return db
495
490
  .prepare(
@@ -500,7 +495,7 @@ export function listSuppressions(db: DatabaseSync | undefined, range: Range = AL
500
495
  }
501
496
 
502
497
  /** Current modules that start in the range, in address order. */
503
- export function listModules(db: DatabaseSync | undefined, range: Range = ALL, origins?: OriginFilter, space: Space = DEFAULT_SPACE): ModuleRow[] {
498
+ export function listModules(db: DatabaseSync | undefined, range: AddressRange = ALL, origins?: OriginFilter, space: Space = DEFAULT_SPACE): ModuleRow[] {
504
499
  if (db === undefined) return [];
505
500
  return db
506
501
  .prepare(`SELECT ${MODULE_COLUMNS} FROM current_modules WHERE space = ? AND start_address BETWEEN ? AND ? ORDER BY start_address`)
@@ -510,7 +505,7 @@ export function listModules(db: DatabaseSync | undefined, range: Range = ALL, or
510
505
  }
511
506
 
512
507
  /** Current operand expressions of instructions in the range, in address order. */
513
- export function listOperands(db: DatabaseSync | undefined, range: Range = ALL, origins?: OriginFilter, space: Space = DEFAULT_SPACE): OperandRow[] {
508
+ export function listOperands(db: DatabaseSync | undefined, range: AddressRange = ALL, origins?: OriginFilter, space: Space = DEFAULT_SPACE): OperandRow[] {
514
509
  if (db === undefined) return [];
515
510
  return db
516
511
  .prepare(`SELECT ${OPERAND_COLUMNS} FROM current_operands WHERE space = ? AND address BETWEEN ? AND ? ORDER BY address`)
@@ -520,7 +515,7 @@ export function listOperands(db: DatabaseSync | undefined, range: Range = ALL, o
520
515
  }
521
516
 
522
517
  /** Current patches that share an address with the range, of one switch or of all, in address order. */
523
- export function listPatches(db: DatabaseSync | undefined, range: Range = ALL, switchName?: string, origins?: OriginFilter, space: Space = DEFAULT_SPACE): PatchRow[] {
518
+ export function listPatches(db: DatabaseSync | undefined, range: AddressRange = ALL, switchName?: string, origins?: OriginFilter, space: Space = DEFAULT_SPACE): PatchRow[] {
524
519
  return patchesOverlapping(db, range.start, range.end, space).filter((patch) => (switchName === undefined || patch.switch === switchName) && kept(origins)(patch));
525
520
  }
526
521
 
@@ -535,7 +530,7 @@ export function listMacros(db: DatabaseSync | undefined, origins?: OriginFilter,
535
530
  }
536
531
 
537
532
  /** Current macro uses that share an address with the range, of one macro or of all, in address order. */
538
- export function listMacroUses(db: DatabaseSync | undefined, range: Range = ALL, macro?: string, origins?: OriginFilter, space: Space = DEFAULT_SPACE): MacroUseRow[] {
533
+ export function listMacroUses(db: DatabaseSync | undefined, range: AddressRange = ALL, macro?: string, origins?: OriginFilter, space: Space = DEFAULT_SPACE): MacroUseRow[] {
539
534
  return macroUsesOverlapping(db, range.start, range.end, space).filter((use) => (macro === undefined || use.macro === macro) && kept(origins)(use));
540
535
  }
541
536
 
@@ -10,6 +10,13 @@ import { OPERAND_SELECTS } from "./expression.ts";
10
10
  /** heuristic: 6502 idioms that the static analysis finds by pattern; an analyzer origin. */
11
11
  export const ORIGINS = ["user", "llm", "dxa", "ghidra", "heuristic"] as const;
12
12
  export type Origin = (typeof ORIGINS)[number];
13
+ /** The origins of semantic knowledge: what the user or the LLM decided. It wins over analyzer facts. */
14
+ export const SEMANTIC_ORIGINS = ["user", "llm"] as const satisfies readonly Origin[];
15
+ /** The analyzers: their facts and names give way to semantic knowledge. */
16
+ export const ANALYZERS = ["dxa", "ghidra", "heuristic"] as const satisfies readonly Origin[];
17
+ export type Analyzer = (typeof ANALYZERS)[number];
18
+ export const isSemantic = (origin: string | undefined): boolean => (SEMANTIC_ORIGINS as readonly (string | undefined)[]).includes(origin);
19
+ export const isAnalyzer = (origin: string | undefined): boolean => (ANALYZERS as readonly (string | undefined)[]).includes(origin);
13
20
  export const SYMBOL_KINDS = ["label", "routine", "variable", "data", "vector"] as const;
14
21
  export type SymbolKind = (typeof SYMBOL_KINDS)[number];
15
22
  export const REGION_TYPES = [
@@ -76,10 +83,9 @@ export function isGeneratedFile(file: string): boolean {
76
83
  }
77
84
 
78
85
  /**
79
- * The address space of a knowledge row: the C64, the 1541 drive, or a
80
- * cartridge bank. Drive code and banks reuse C64 addresses.
86
+ * The address space of a knowledge row: a name that SPACE_PATTERN of c64.ts
87
+ * accepts. Drive code and banks reuse C64 addresses.
81
88
  */
82
- export const SPACE = /^(c64|drive8|bank(0|[1-9][0-9]?|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$/;
83
89
  export type Space = string;
84
90
  export const DEFAULT_SPACE: Space = "c64";
85
91
  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]'";
@@ -10,8 +10,8 @@ import { readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "no
10
10
  import type { DatabaseSync } from "node:sqlite";
11
11
  import { join } from "node:path";
12
12
 
13
+ import { Refusal } from "../errors.ts";
13
14
  import { isoCet } from "../time.ts";
14
- import { KnowledgeError } from "./database.ts";
15
15
 
16
16
  /** The value of "no revision": empty knowledge. It sorts before every revision id. */
17
17
  export const NO_REVISION = "0";
@@ -248,7 +248,7 @@ export function readText(directory: string): Map<string, { text: string; sha256:
248
248
  const files = new Map<string, { text: string; sha256: string }>();
249
249
  for (const name of readdirSync(directory).sort()) {
250
250
  if (!name.endsWith(".jsonl")) continue;
251
- if (!known.has(name)) throw new KnowledgeError("invalid-database", `.c64-re-tools/knowledge/${name} is not a knowledge file. Knowledge files are ${[...known].join(", ")}.`);
251
+ if (!known.has(name)) throw new Refusal("invalid-database", `.c64-re-tools/knowledge/${name} is not a knowledge file. Knowledge files are ${[...known].join(", ")}.`);
252
252
  const bytes = readFileSync(join(directory, name));
253
253
  files.set(name, { text: bytes.toString("utf8"), sha256: sha256(bytes) });
254
254
  }
@@ -269,7 +269,7 @@ export function writeAtomic(path: string, text: string): void {
269
269
 
270
270
  /** One line of a knowledge file that cannot be read. */
271
271
  function bad(file: string, line: number, message: string): never {
272
- throw new KnowledgeError("invalid-database", `.c64-re-tools/knowledge/${file}:${line}: ${message}`);
272
+ throw new Refusal("invalid-database", `.c64-re-tools/knowledge/${file}:${line}: ${message}`);
273
273
  }
274
274
 
275
275
  function parseLines(file: string, text: string): Array<{ line: number; value: Raw }> {
@@ -396,7 +396,7 @@ export function loadText(db: DatabaseSync, files: ReadonlyMap<string, { text: st
396
396
  insertRevision.run(...(REVISION_FIELDS.map((item) => revision.columns[item.column]) as Array<string | null>));
397
397
  for (const item of revision.evidence) insertEvidence.run(revision.id, item.kind as string, item.ref as string, item.sha256 as string | null, item.space as string | null);
398
398
  } catch (error) {
399
- throw new KnowledgeError("invalid-database", `.c64-re-tools/knowledge/${REVISIONS_FILE}: the revision ${revision.id} is not valid: ${(error as Error).message}`);
399
+ throw new Refusal("invalid-database", `.c64-re-tools/knowledge/${REVISIONS_FILE}: the revision ${revision.id} is not valid: ${(error as Error).message}`);
400
400
  }
401
401
  }
402
402
  for (const spec of TABLES) {
@@ -408,7 +408,7 @@ export function loadText(db: DatabaseSync, files: ReadonlyMap<string, { text: st
408
408
  try {
409
409
  insert.run(...(columns.map((column) => row.columns[column]) as Array<string | number | null>));
410
410
  } catch (error) {
411
- throw new KnowledgeError("invalid-database", `.c64-re-tools/knowledge/${spec.file}: the row ${row.id} is not valid: ${(error as Error).message}`);
411
+ throw new Refusal("invalid-database", `.c64-re-tools/knowledge/${spec.file}: the row ${row.id} is not valid: ${(error as Error).message}`);
412
412
  }
413
413
  }
414
414
  }