@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
@@ -3,11 +3,12 @@
3
3
 
4
4
  import type { DatabaseSync } from "node:sqlite";
5
5
 
6
- import { instructionLength, SYMBOL_NAME } from "../c64.ts";
6
+ import { Refusal } from "../errors.ts";
7
+ import { type AddressRange, formatC64Address, instructionLength, SPACE_PATTERN, SYMBOL_NAME } from "../c64.ts";
7
8
  import { isoCet } from "../time.ts";
8
9
  import { CONFLICT_RULES, conflicts, conflictTable, type Conflict, type ConflictRow } from "./conflicts.ts";
9
- import { KnowledgeError, transaction } from "./database.ts";
10
- import { checkExpression, evaluate, ExpressionError, identifiers, operandText, replaceIdentifiers } from "./expression.ts";
10
+ import { transaction } from "./database.ts";
11
+ import { checkExpression, evaluate, identifiers, operandText, replaceIdentifiers } from "./expression.ts";
11
12
  import {
12
13
  COMMENT_COLUMNS,
13
14
  commentsAt,
@@ -58,33 +59,7 @@ import {
58
59
  type SuppressionRow,
59
60
  type SymbolRow,
60
61
  } from "./read.ts";
61
- import {
62
- COMMENT_PLACEMENTS,
63
- EVIDENCE_KINDS,
64
- MAX_ELEMENT,
65
- MODULE_FILE,
66
- DEFAULT_SPACE,
67
- GENERATED_FILES,
68
- isGeneratedFile,
69
- MACRO_NAME,
70
- MACRO_PARAM,
71
- SPACE,
72
- SWITCH_NAME,
73
- NO_ELEMENT_TYPES,
74
- OPERAND_SELECTS,
75
- POINTER_KIND,
76
- REFERENCE_KINDS,
77
- REGION_TYPES,
78
- SYMBOL_KINDS,
79
- type CommentPlacement,
80
- type EvidenceKind,
81
- type OperandSelect,
82
- type Origin,
83
- type ReferenceKind,
84
- type RegionType,
85
- type Space,
86
- type SymbolKind,
87
- } from "./schema.ts";
62
+ import { COMMENT_PLACEMENTS, EVIDENCE_KINDS, MAX_ELEMENT, MODULE_FILE, DEFAULT_SPACE, GENERATED_FILES, isGeneratedFile, MACRO_NAME, MACRO_PARAM, SWITCH_NAME, NO_ELEMENT_TYPES, OPERAND_SELECTS, POINTER_KIND, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type CommentPlacement, type EvidenceKind, type OperandSelect, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
88
63
  import { newRevisionId, rowId, sqlName } from "./text.ts";
89
64
 
90
65
  /** What a change rests on: a coverage run, a test or another project file with its SHA-256, or a note. */
@@ -132,20 +107,11 @@ const MAX_PARAMS = 16;
132
107
  const MAX_EVIDENCE_REF = 1000;
133
108
 
134
109
  export function checkAddress(value: number, what = "address"): void {
135
- if (!Number.isInteger(value) || value < 0 || value > 0xffff) throw new KnowledgeError("invalid-input", `${what} must be from $0000 to $ffff.`);
110
+ if (!Number.isInteger(value) || value < 0 || value > 0xffff) throw new Refusal("invalid-input", `${what} must be from $0000 to $ffff.`);
136
111
  }
137
112
 
138
113
  function checkOneOf<T extends string>(values: readonly T[], value: string, what: string): asserts value is T {
139
- if (!(values as readonly string[]).includes(value)) throw new KnowledgeError("invalid-input", `${what} must be one of: ${values.join(", ")}.`);
140
- }
141
-
142
- export function hex(value: number): string {
143
- return `$${value.toString(16).padStart(4, "0")}`;
144
- }
145
-
146
- export interface AddressRange {
147
- start: number;
148
- end: number;
114
+ if (!(values as readonly string[]).includes(value)) throw new Refusal("invalid-input", `${what} must be one of: ${values.join(", ")}.`);
149
115
  }
150
116
 
151
117
  /** Parts of `range` that no range in `holes` covers. */
@@ -191,14 +157,14 @@ export class Change {
191
157
  // A merge of two computers' work can leave conflicts: only a merge revision writes then.
192
158
  const found = operation === "merge" ? [] : conflicts(db);
193
159
  if (found.length > 0) {
194
- throw new KnowledgeError(
160
+ throw new Refusal(
195
161
  "conflict",
196
162
  `The knowledge has ${found.length} conflict${found.length === 1 ? "" : "s"} from a git merge, for example ${describe(found[0]!)}. Nothing was written. Run knowledge.ts merge to decide them, then write again.`,
197
163
  );
198
164
  }
199
165
  const current = currentRevision(db);
200
166
  if (context.expectedRevision !== undefined && resolveRevision(db, context.expectedRevision) !== current) {
201
- throw new KnowledgeError(
167
+ throw new Refusal(
202
168
  "stale-revision",
203
169
  `Knowledge changed since revision ${context.expectedRevision}; it is now at revision ${current}. Read it again and decide again.`,
204
170
  );
@@ -226,7 +192,7 @@ export class Change {
226
192
  check(): void {
227
193
  if (this.revision === null) return;
228
194
  const [found] = conflicts(this.#db);
229
- if (found !== undefined) throw new KnowledgeError("conflict", `The write would leave ${describe(found)}. Nothing was written.`);
195
+ if (found !== undefined) throw new Refusal("conflict", `The write would leave ${describe(found)}. Nothing was written.`);
230
196
  }
231
197
 
232
198
  /** The id of the next row this write inserts. */
@@ -327,9 +293,9 @@ function describe(conflict: Conflict): string {
327
293
  const place = (entry: ConflictRow) => {
328
294
  if (entry.entity === "macro") return entry.row.name;
329
295
  const row = entry.row as { address?: number; start?: number; end?: number; from?: number };
330
- if (row.address !== undefined) return hex(row.address);
331
- if (row.from !== undefined && entry.entity === "reference") return hex(row.from);
332
- return row.end === undefined || row.end === row.start ? hex(row.start!) : `${hex(row.start!)}-${hex(row.end)}`;
296
+ if (row.address !== undefined) return formatC64Address(row.address);
297
+ if (row.from !== undefined && entry.entity === "reference") return formatC64Address(row.from);
298
+ return row.end === undefined || row.end === row.start ? formatC64Address(row.start!) : `${formatC64Address(row.start!)}-${formatC64Address(row.end)}`;
333
299
  };
334
300
  return `${CONFLICT_RULES[conflict.rule]} in ${conflict.space} (${[...new Set(conflict.rows.map(place))].join(", ")})`;
335
301
  }
@@ -348,8 +314,8 @@ function liftRegionSuppressions(db: DatabaseSync, change: Change, start: number,
348
314
  function checkEvidence(evidence: Evidence[] | undefined): void {
349
315
  for (const item of evidence ?? []) {
350
316
  checkOneOf(EVIDENCE_KINDS, item.kind, "evidence kind");
351
- if (item.ref.trim().length === 0 || item.ref.length > MAX_EVIDENCE_REF) throw new KnowledgeError("invalid-input", `evidence has 1 to ${MAX_EVIDENCE_REF} characters after its kind.`);
352
- if (item.sha256 !== undefined && !/^[0-9a-f]{64}$/.test(item.sha256)) throw new KnowledgeError("invalid-input", "evidence sha256 must be 64 lowercase hex digits.");
317
+ if (item.ref.trim().length === 0 || item.ref.length > MAX_EVIDENCE_REF) throw new Refusal("invalid-input", `evidence has 1 to ${MAX_EVIDENCE_REF} characters after its kind.`);
318
+ if (item.sha256 !== undefined && !/^[0-9a-f]{64}$/.test(item.sha256)) throw new Refusal("invalid-input", "evidence sha256 must be 64 lowercase hex digits.");
353
319
  checkSpace(item);
354
320
  }
355
321
  }
@@ -357,7 +323,7 @@ function checkEvidence(evidence: Evidence[] | undefined): void {
357
323
  /** Runs semantic writes in one transaction, as one revision. */
358
324
  export function write<T>(db: DatabaseSync, context: WriteContext, operation: string, work: (change: Change) => T): { revision: string | null; current: T } {
359
325
  if (context.origin !== "user" && context.origin !== "llm") {
360
- throw new KnowledgeError("invalid-input", "A semantic write comes from the user or the LLM.");
326
+ throw new Refusal("invalid-input", "A semantic write comes from the user or the LLM.");
361
327
  }
362
328
  checkEvidence(context.evidence);
363
329
  return transaction(db, () => {
@@ -393,14 +359,14 @@ type AddressRequest = InSpace & { address: number };
393
359
  const spaceOf = (request: InSpace): Space => request.space ?? DEFAULT_SPACE;
394
360
 
395
361
  export function checkSpace(request: InSpace): void {
396
- if (request.space !== undefined && !SPACE.test(request.space)) throw new KnowledgeError("invalid-input", "space must be c64, drive8, or bank0 to bank255.");
362
+ if (request.space !== undefined && !SPACE_PATTERN.test(request.space)) throw new Refusal("invalid-input", "space must be c64, drive8, or bank0 to bank255.");
397
363
  }
398
364
 
399
365
  function checkSymbol(request: SymbolRequest): void {
400
366
  checkSpace(request);
401
367
  checkAddress(request.address);
402
368
  if (!SYMBOL_NAME.test(request.name)) {
403
- throw new KnowledgeError("invalid-input", "A symbol name is a letter or underscore, then letters, digits or underscores (at most 64), with an optional leading dot.");
369
+ throw new Refusal("invalid-input", "A symbol name is a letter or underscore, then letters, digits or underscores (at most 64), with an optional leading dot.");
404
370
  }
405
371
  if (request.kind !== undefined) checkOneOf(SYMBOL_KINDS, request.kind, "kind");
406
372
  }
@@ -412,10 +378,10 @@ function renameStep(db: DatabaseSync, change: Change, origin: Origin, request: S
412
378
  if (existing !== undefined && existing.name === request.name && existing.kind === kind) return { current: existing };
413
379
  const owner = symbolNamed(db, request.name, space);
414
380
  if (owner !== undefined && owner.address !== request.address) {
415
- throw new KnowledgeError("conflict", `The name ${request.name} already belongs to ${hex(owner.address)}. Rename or remove that symbol first.`);
381
+ throw new Refusal("conflict", `The name ${request.name} already belongs to ${formatC64Address(owner.address)}. Rename or remove that symbol first.`);
416
382
  }
417
383
  const [patch] = listPatches(db, undefined, request.name, undefined, space);
418
- if (patch !== undefined) throw new KnowledgeError("conflict", `${request.name} is the switch of the patch at ${hex(patch.start)}-${hex(patch.end)}. Give the symbol another name.`);
384
+ if (patch !== undefined) throw new Refusal("conflict", `${request.name} is the switch of the patch at ${formatC64Address(patch.start)}-${formatC64Address(patch.end)}. Give the symbol another name.`);
419
385
  if (existing !== undefined) change.close("symbols", "space = ? AND address = ?", space, request.address);
420
386
  change.insertSymbol({ space, address: request.address, name: request.name, kind, origin });
421
387
  if (existing !== undefined && existing.name !== request.name) {
@@ -430,17 +396,17 @@ function renameStep(db: DatabaseSync, change: Change, origin: Origin, request: S
430
396
  function removeSymbolStep(db: DatabaseSync, change: Change, origin: Origin, request: AddressRequest): Step<null> {
431
397
  const space = spaceOf(request);
432
398
  const existing = symbolAt(db, request.address, space);
433
- if (existing === undefined) throw new KnowledgeError("not-found", `There is no symbol at ${hex(request.address)}.`);
399
+ if (existing === undefined) throw new Refusal("not-found", `There is no symbol at ${formatC64Address(request.address)}.`);
434
400
  const users = operandsNaming(db, space, existing.name);
435
401
  if (users.length > 0) {
436
- throw new KnowledgeError(
402
+ throw new Refusal(
437
403
  "conflict",
438
- `The operand expressions at ${users.map((row) => hex(row.address)).join(", ")} use ${existing.name}. Change or remove them first (unoperand), or rename the symbol.`,
404
+ `The operand expressions at ${users.map((row) => formatC64Address(row.address)).join(", ")} use ${existing.name}. Change or remove them first (unoperand), or rename the symbol.`,
439
405
  );
440
406
  }
441
407
  const uses = usesNaming(db, space, existing.name);
442
408
  if (uses.length > 0) {
443
- throw new KnowledgeError("conflict", `The macro uses at ${uses.map((row) => hex(row.start)).join(", ")} use ${existing.name}. Change or remove them first (unuse), or rename the symbol.`);
409
+ throw new Refusal("conflict", `The macro uses at ${uses.map((row) => formatC64Address(row.start)).join(", ")} use ${existing.name}. Change or remove them first (unuse), or rename the symbol.`);
444
410
  }
445
411
  // No suppression: an analyzer name is a second name and never comes back as the name.
446
412
  change.close("symbols", "space = ? AND address = ?", space, request.address);
@@ -451,17 +417,17 @@ function checkRange(request: RangeRequest): void {
451
417
  checkSpace(request);
452
418
  checkAddress(request.start, "start");
453
419
  checkAddress(request.end, "end");
454
- if (request.end < request.start) throw new KnowledgeError("invalid-input", "end must not be before start.");
420
+ if (request.end < request.start) throw new Refusal("invalid-input", "end must not be before start.");
455
421
  }
456
422
 
457
423
  function checkRegion(request: RegionRequest): void {
458
424
  checkRange(request);
459
425
  checkOneOf(REGION_TYPES, request.type, "type");
460
426
  if (request.element === undefined) return;
461
- if (NO_ELEMENT_TYPES.includes(request.type)) throw new KnowledgeError("invalid-input", `A ${request.type} region has no element size.`);
462
- if (!Number.isInteger(request.element) || request.element < 1 || request.element > MAX_ELEMENT) throw new KnowledgeError("invalid-input", `element must be from 1 to ${MAX_ELEMENT}.`);
427
+ if (NO_ELEMENT_TYPES.includes(request.type)) throw new Refusal("invalid-input", `A ${request.type} region has no element size.`);
428
+ if (!Number.isInteger(request.element) || request.element < 1 || request.element > MAX_ELEMENT) throw new Refusal("invalid-input", `element must be from 1 to ${MAX_ELEMENT}.`);
463
429
  const length = request.end - request.start + 1;
464
- if (length % request.element !== 0) throw new KnowledgeError("invalid-input", `The region has ${length} bytes, which is not a multiple of the element size ${request.element}.`);
430
+ if (length % request.element !== 0) throw new Refusal("invalid-input", `The region has ${length} bytes, which is not a multiple of the element size ${request.element}.`);
465
431
  }
466
432
 
467
433
  /** The element size of a piece of a region: kept when the piece still holds whole elements. */
@@ -499,7 +465,7 @@ function classifyStep(db: DatabaseSync, change: Change, origin: Origin, request:
499
465
  function unclassifyStep(db: DatabaseSync, change: Change, origin: Origin, request: RangeRequest): Step<null> {
500
466
  const space = spaceOf(request);
501
467
  if (regionsOverlapping(db, request.start, request.end, space).length === 0) {
502
- throw new KnowledgeError("not-found", `No region covers ${hex(request.start)}-${hex(request.end)}.`);
468
+ throw new Refusal("not-found", `No region covers ${formatC64Address(request.start)}-${formatC64Address(request.end)}.`);
503
469
  }
504
470
  clearRange(db, change, request.start, request.end, space);
505
471
  // Suppress only the parts that no region suppression holds yet, so suppressions never overlap.
@@ -514,7 +480,7 @@ function checkComment(request: CommentRequest): string {
514
480
  checkAddress(request.address);
515
481
  checkOneOf(COMMENT_PLACEMENTS, request.placement, "placement");
516
482
  const text = request.text.trim();
517
- if (text.length === 0 || text.length > MAX_COMMENT) throw new KnowledgeError("invalid-input", `A comment has 1 to ${MAX_COMMENT} characters.`);
483
+ if (text.length === 0 || text.length > MAX_COMMENT) throw new Refusal("invalid-input", `A comment has 1 to ${MAX_COMMENT} characters.`);
518
484
  return text;
519
485
  }
520
486
 
@@ -537,7 +503,7 @@ function checkPlacement(request: PlacementRequest): void {
537
503
  function uncommentStep(db: DatabaseSync, change: Change, _origin: Origin, request: PlacementRequest): Step<null> {
538
504
  const space = spaceOf(request);
539
505
  if (!commentsAt(db, request.address, space).some((comment) => comment.placement === request.placement)) {
540
- throw new KnowledgeError("not-found", `There is no ${request.placement} comment at ${hex(request.address)}.`);
506
+ throw new Refusal("not-found", `There is no ${request.placement} comment at ${formatC64Address(request.address)}.`);
541
507
  }
542
508
  change.close("comments", "space = ? AND address = ? AND placement = ?", space, request.address, request.placement);
543
509
  return { current: null };
@@ -548,7 +514,7 @@ function checkReference(request: ReferenceRequest): void {
548
514
  checkAddress(request.from, "from");
549
515
  checkAddress(request.to, "to");
550
516
  checkOneOf(REFERENCE_KINDS, request.kind, "kind");
551
- if (request.kind === POINTER_KIND) throw new KnowledgeError("invalid-input", "Only an operand expression writes a pointer reference: use operand and unoperand.");
517
+ if (request.kind === POINTER_KIND) throw new Refusal("invalid-input", "Only an operand expression writes a pointer reference: use operand and unoperand.");
552
518
  }
553
519
 
554
520
  function referenceStep(db: DatabaseSync, change: Change, origin: Origin, request: ReferenceRequest): Step<ReferenceRow> {
@@ -564,7 +530,7 @@ function referenceStep(db: DatabaseSync, change: Change, origin: Origin, request
564
530
  function unreferenceStep(db: DatabaseSync, change: Change, origin: Origin, request: ReferenceRequest): Step<null> {
565
531
  const space = spaceOf(request);
566
532
  if (!referencesFrom(db, request.from, space).some((reference) => reference.to === request.to && reference.kind === request.kind)) {
567
- throw new KnowledgeError("not-found", `There is no ${request.kind} reference from ${hex(request.from)} to ${hex(request.to)}.`);
533
+ throw new Refusal("not-found", `There is no ${request.kind} reference from ${formatC64Address(request.from)} to ${formatC64Address(request.to)}.`);
568
534
  }
569
535
  change.close('"references"', "space = ? AND from_address = ? AND to_address = ? AND kind = ?", space, request.from, request.to, request.kind);
570
536
  change.insertSuppression({ space, entity: "reference", start: request.from, end: request.from, to: request.to, kind: request.kind, origin: origin as SuppressionRow["origin"] });
@@ -594,19 +560,14 @@ function checkOperand(request: OperandRequest): void {
594
560
  checkAddress(request.address);
595
561
  checkOneOf(OPERAND_SELECTS, request.select, "select");
596
562
  if (request.select === "expr") {
597
- if (request.symbol !== undefined || request.offset !== undefined) throw new KnowledgeError("invalid-input", "An expr operand has expression text, and no symbol or offset.");
598
- if (typeof request.expression !== "string") throw new KnowledgeError("invalid-input", "An expr operand has expression text.");
599
- try {
600
- checkExpression(request.expression);
601
- } catch (error) {
602
- if (error instanceof ExpressionError) throw new KnowledgeError("invalid-input", error.message);
603
- throw error;
604
- }
563
+ if (request.symbol !== undefined || request.offset !== undefined) throw new Refusal("invalid-input", "An expr operand has expression text, and no symbol or offset.");
564
+ if (typeof request.expression !== "string") throw new Refusal("invalid-input", "An expr operand has expression text.");
565
+ checkExpression(request.expression);
605
566
  return;
606
567
  }
607
- if (request.expression !== undefined) throw new KnowledgeError("invalid-input", `A ${request.select} operand has a symbol and an optional offset, and no expression text.`);
608
- if (request.symbol === undefined || !SYMBOL_NAME.test(request.symbol)) throw new KnowledgeError("invalid-input", `A ${request.select} operand names a symbol: a letter or underscore, then letters, digits or underscores.`);
609
- if (request.offset !== undefined && (!Number.isInteger(request.offset) || Math.abs(request.offset) > 0xffff)) throw new KnowledgeError("invalid-input", "offset must be a whole number from -$ffff to $ffff.");
568
+ if (request.expression !== undefined) throw new Refusal("invalid-input", `A ${request.select} operand has a symbol and an optional offset, and no expression text.`);
569
+ if (request.symbol === undefined || !SYMBOL_NAME.test(request.symbol)) throw new Refusal("invalid-input", `A ${request.select} operand names a symbol: a letter or underscore, then letters, digits or underscores.`);
570
+ if (request.offset !== undefined && (!Number.isInteger(request.offset) || Math.abs(request.offset) > 0xffff)) throw new Refusal("invalid-input", "offset must be a whole number from -$ffff to $ffff.");
610
571
  }
611
572
 
612
573
  const shown = (value: number, size: number) => (value < 0 ? String(value) : `$${value.toString(16).padStart(size === 0x100 ? 2 : 4, "0")}`);
@@ -617,36 +578,36 @@ const shown = (value: number, size: number) => (value < 0 ? String(value) : `$${
617
578
  * the platform names of the C64 space.
618
579
  */
619
580
  function checkOperandValue(db: DatabaseSync, space: Space, image: OperandImage, request: OperandRequest, text: string): number[] {
620
- const where = hex(request.address);
581
+ const where = formatC64Address(request.address);
621
582
  const opcode = image.byteAt(request.address);
622
- if (opcode === undefined) throw new KnowledgeError("invalid-input", `${where} is outside the image.`);
583
+ if (opcode === undefined) throw new Refusal("invalid-input", `${where} is outside the image.`);
623
584
  const length = instructionLength(opcode);
624
- if (length === 1) throw new KnowledgeError("invalid-input", `The instruction at ${where} (opcode ${shown(opcode, 0x100)}) has no operand.`);
625
- if ((opcode & 0x1f) === 0x10) throw new KnowledgeError("invalid-input", `The instruction at ${where} is a branch: its operand is a distance, not a value.`);
585
+ if (length === 1) throw new Refusal("invalid-input", `The instruction at ${where} (opcode ${shown(opcode, 0x100)}) has no operand.`);
586
+ if ((opcode & 0x1f) === 0x10) throw new Refusal("invalid-input", `The instruction at ${where} is a branch: its operand is a distance, not a value.`);
626
587
  if ((request.select === "lo" || request.select === "hi") && length === 3) {
627
- throw new KnowledgeError("invalid-input", `lo and hi give one byte, but the instruction at ${where} has a 2-byte operand. Use value.`);
588
+ throw new Refusal("invalid-input", `lo and hi give one byte, but the instruction at ${where} has a 2-byte operand. Use value.`);
628
589
  }
629
590
  const bytes = [image.byteAt(request.address + 1), length === 3 ? image.byteAt(request.address + 2) : 0];
630
- if (bytes.some((byte) => byte === undefined)) throw new KnowledgeError("invalid-input", `The instruction at ${where} runs past the end of the image.`);
591
+ if (bytes.some((byte) => byte === undefined)) throw new Refusal("invalid-input", `The instruction at ${where} runs past the end of the image.`);
631
592
  const operand = bytes[0]! | (bytes[1]! << 8);
632
593
  const resolve = (name: string) => symbolNamed(db, name, space)?.address ?? (space === DEFAULT_SPACE ? image.platform.get(name) : undefined);
633
594
  let value: number;
634
595
  try {
635
596
  value = evaluate(text, resolve);
636
597
  } catch (error) {
637
- if (error instanceof ExpressionError) throw new KnowledgeError("invalid-input", `${where}: ${error.message}`);
598
+ if (error instanceof Refusal) throw new Refusal(error.code, `${where}: ${error.message}`);
638
599
  throw error;
639
600
  }
640
601
  const size = length === 2 ? 0x100 : 0x10000;
641
602
  if (value < -size / 2 || value >= size || ((value % size) + size) % size !== operand) {
642
- throw new KnowledgeError("invalid-input", `At ${where}, ${text} gives ${shown(value, size)}, but the operand in the image is ${shown(operand, size)}.`);
603
+ throw new Refusal("invalid-input", `At ${where}, ${text} gives ${shown(value, size)}, but the operand in the image is ${shown(operand, size)}.`);
643
604
  }
644
605
  return identifiers(text).map((name) => resolve(name)!);
645
606
  }
646
607
 
647
608
  function operandStep(db: DatabaseSync, change: Change, context: ChangeContext & { image?: OperandImage }, request: OperandRequest): Step<OperandRow, OperandRow> {
648
609
  const space = spaceOf(request);
649
- if (context.image === undefined) throw new KnowledgeError("invalid-input", "An operand expression is checked against the image, and no image was given.");
610
+ if (context.image === undefined) throw new Refusal("invalid-input", "An operand expression is checked against the image, and no image was given.");
650
611
  const wanted: Omit<OperandRow, "space" | "address" | "origin" | "revision"> = { select: request.select, offset: request.offset ?? 0 };
651
612
  if (request.symbol !== undefined) wanted.symbol = request.symbol;
652
613
  if (request.expression !== undefined) wanted.expression = request.expression.trim();
@@ -671,7 +632,7 @@ function operandStep(db: DatabaseSync, change: Change, context: ChangeContext &
671
632
 
672
633
  function unoperandStep(db: DatabaseSync, change: Change, _origin: Origin, request: AddressRequest): Step<null> {
673
634
  const space = spaceOf(request);
674
- if (operandAt(db, request.address, space) === undefined) throw new KnowledgeError("not-found", `There is no operand expression at ${hex(request.address)}.`);
635
+ if (operandAt(db, request.address, space) === undefined) throw new Refusal("not-found", `There is no operand expression at ${formatC64Address(request.address)}.`);
675
636
  change.close("operands", "space = ? AND address = ?", space, request.address);
676
637
  change.close('"references"', "space = ? AND from_address = ? AND kind = ?", space, request.address, POINTER_KIND);
677
638
  return { current: null };
@@ -682,11 +643,11 @@ type ModuleRequest = InSpace & { start: number; file: string; title?: string };
682
643
  function checkModule(request: ModuleRequest): void {
683
644
  checkSpace(request);
684
645
  checkAddress(request.start, "start");
685
- if (!MODULE_FILE.test(request.file)) throw new KnowledgeError("invalid-input", "A module file is a name of letters, digits, underscores or hyphens (at most 60) that ends in .asm, without a directory.");
646
+ if (!MODULE_FILE.test(request.file)) throw new Refusal("invalid-input", "A module file is a name of letters, digits, underscores or hyphens (at most 60) that ends in .asm, without a directory.");
686
647
  if (isGeneratedFile(request.file)) {
687
- throw new KnowledgeError("invalid-input", `c64-source writes ${GENERATED_FILES.join(", ")} itself, in any letter case, so a module cannot have the file ${request.file}. Give the module another file.`);
648
+ throw new Refusal("invalid-input", `c64-source writes ${GENERATED_FILES.join(", ")} itself, in any letter case, so a module cannot have the file ${request.file}. Give the module another file.`);
688
649
  }
689
- if (request.title !== undefined && (request.title.trim().length === 0 || request.title.length > MAX_COMMENT)) throw new KnowledgeError("invalid-input", `A module title has 1 to ${MAX_COMMENT} characters.`);
650
+ if (request.title !== undefined && (request.title.trim().length === 0 || request.title.length > MAX_COMMENT)) throw new Refusal("invalid-input", `A module title has 1 to ${MAX_COMMENT} characters.`);
690
651
  }
691
652
 
692
653
  function moduleStep(db: DatabaseSync, change: Change, origin: Origin, request: ModuleRequest): Step<ModuleRow> {
@@ -697,7 +658,7 @@ function moduleStep(db: DatabaseSync, change: Change, origin: Origin, request: M
697
658
  if (existing !== undefined && existing.file === request.file && existing.title === title) return { current: existing };
698
659
  const owner = moduleWithFile(db, request.file, space);
699
660
  if (owner !== undefined && owner.start !== request.start) {
700
- throw new KnowledgeError("conflict", `The file ${request.file} already belongs to the module at ${hex(owner.start)}. Give this module another file, or remove that module first.`);
661
+ throw new Refusal("conflict", `The file ${request.file} already belongs to the module at ${formatC64Address(owner.start)}. Give this module another file, or remove that module first.`);
701
662
  }
702
663
  if (existing !== undefined) change.close("modules", "space = ? AND start_address = ?", space, request.start);
703
664
  change.insertModule({ space, start: request.start, file: request.file, ...(title === undefined ? {} : { title }), origin: origin as ModuleRow["origin"] });
@@ -706,7 +667,7 @@ function moduleStep(db: DatabaseSync, change: Change, origin: Origin, request: M
706
667
 
707
668
  function unmoduleStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { start: number }): Step<null> {
708
669
  const space = spaceOf(request);
709
- if (moduleAt(db, request.start, space) === undefined) throw new KnowledgeError("not-found", `No module starts at ${hex(request.start)}.`);
670
+ if (moduleAt(db, request.start, space) === undefined) throw new Refusal("not-found", `No module starts at ${formatC64Address(request.start)}.`);
710
671
  change.close("modules", "space = ? AND start_address = ?", space, request.start);
711
672
  return { current: null };
712
673
  }
@@ -729,20 +690,20 @@ function checkBraces(text: string, what: string): void {
729
690
  if (character === ";") break;
730
691
  if (character === '"' || character === "'") quote = character;
731
692
  else if (character === "{") depth++;
732
- else if (character === "}" && --depth < 0) throw new KnowledgeError("invalid-input", `${what} has a } with no { before it.`);
693
+ else if (character === "}" && --depth < 0) throw new Refusal("invalid-input", `${what} has a } with no { before it.`);
733
694
  }
734
695
  }
735
- if (depth !== 0) throw new KnowledgeError("invalid-input", `${what} has a { with no }.`);
696
+ if (depth !== 0) throw new Refusal("invalid-input", `${what} has a { with no }.`);
736
697
  }
737
698
 
738
699
  function checkCommentText(comment: string | undefined, what: string): void {
739
- if (comment !== undefined && (comment.trim().length === 0 || comment.length > MAX_COMMENT)) throw new KnowledgeError("invalid-input", `${what} has 1 to ${MAX_COMMENT} characters.`);
700
+ if (comment !== undefined && (comment.trim().length === 0 || comment.length > MAX_COMMENT)) throw new Refusal("invalid-input", `${what} has 1 to ${MAX_COMMENT} characters.`);
740
701
  }
741
702
 
742
703
  /** Refuses a range with a byte outside the image. */
743
704
  function checkInImage(image: OperandImage, start: number, end: number, what: string): void {
744
705
  for (let address = start; address <= end; address++) {
745
- if (image.byteAt(address) === undefined) throw new KnowledgeError("invalid-input", `${hex(address)} is outside the image: ${what} is a range of the image.`);
706
+ if (image.byteAt(address) === undefined) throw new Refusal("invalid-input", `${formatC64Address(address)} is outside the image: ${what} is a range of the image.`);
746
707
  }
747
708
  }
748
709
 
@@ -751,28 +712,28 @@ type PatchRequest = InSpace & { start: number; end: number; switch: string; code
751
712
  function checkPatch(request: PatchRequest): void {
752
713
  checkRange(request);
753
714
  if (typeof request.switch !== "string" || !SWITCH_NAME.test(request.switch)) {
754
- throw new KnowledgeError("invalid-input", "A switch name is an upper-case letter, then 1 to 31 upper-case letters, digits or underscores, for example CRACK.");
715
+ throw new Refusal("invalid-input", "A switch name is an upper-case letter, then 1 to 31 upper-case letters, digits or underscores, for example CRACK.");
755
716
  }
756
- if (typeof request.code !== "string" || request.code.length > MAX_CODE) throw new KnowledgeError("invalid-input", `Patch code has at most ${MAX_CODE} characters; empty code fills the range with NOP.`);
717
+ if (typeof request.code !== "string" || request.code.length > MAX_CODE) throw new Refusal("invalid-input", `Patch code has at most ${MAX_CODE} characters; empty code fills the range with NOP.`);
757
718
  checkBraces(request.code, "The patch code");
758
719
  checkCommentText(request.comment, "A patch comment");
759
720
  }
760
721
 
761
722
  function patchStep(db: DatabaseSync, change: Change, context: ChangeContext & { image?: OperandImage }, request: PatchRequest): Step<PatchRow, PatchRow> {
762
723
  const space = spaceOf(request);
763
- const range = `${hex(request.start)}-${hex(request.end)}`;
764
- if (context.image === undefined) throw new KnowledgeError("invalid-input", "A patch is checked against the image, and no image was given.");
724
+ const range = `${formatC64Address(request.start)}-${formatC64Address(request.end)}`;
725
+ if (context.image === undefined) throw new Refusal("invalid-input", "A patch is checked against the image, and no image was given.");
765
726
  checkInImage(context.image, request.start, request.end, "a patch");
766
727
  const symbol = symbolNamed(db, request.switch, space);
767
- if (symbol !== undefined) throw new KnowledgeError("conflict", `The switch name ${request.switch} is the name of the symbol at ${hex(symbol.address)}. Give the switch another name.`);
768
- if (space === DEFAULT_SPACE && context.image.platform.has(request.switch)) throw new KnowledgeError("conflict", `The switch name ${request.switch} is a C64 platform name. Give the switch another name.`);
728
+ if (symbol !== undefined) throw new Refusal("conflict", `The switch name ${request.switch} is the name of the symbol at ${formatC64Address(symbol.address)}. Give the switch another name.`);
729
+ if (space === DEFAULT_SPACE && context.image.platform.has(request.switch)) throw new Refusal("conflict", `The switch name ${request.switch} is a C64 platform name. Give the switch another name.`);
769
730
  const other = patchesOverlapping(db, request.start, request.end, space).find((row) => row.start !== request.start);
770
731
  if (other !== undefined) {
771
- throw new KnowledgeError("conflict", `The patch ${range} overlaps the patch of ${other.switch} at ${hex(other.start)}-${hex(other.end)}. Change or remove that patch first (unpatch).`);
732
+ throw new Refusal("conflict", `The patch ${range} overlaps the patch of ${other.switch} at ${formatC64Address(other.start)}-${formatC64Address(other.end)}. Change or remove that patch first (unpatch).`);
772
733
  }
773
734
  const [use] = macroUsesOverlapping(db, request.start, request.end, space);
774
735
  if (use !== undefined) {
775
- throw new KnowledgeError("conflict", `The patch ${range} overlaps the use of the macro ${use.macro} at ${hex(use.start)}-${hex(use.end)}. Remove that use first (unuse).`);
736
+ throw new Refusal("conflict", `The patch ${range} overlaps the use of the macro ${use.macro} at ${formatC64Address(use.start)}-${formatC64Address(use.end)}. Remove that use first (unuse).`);
776
737
  }
777
738
  const existing = patchesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start);
778
739
  const code = request.code.trimEnd();
@@ -788,7 +749,7 @@ function patchStep(db: DatabaseSync, change: Change, context: ChangeContext & {
788
749
 
789
750
  function unpatchStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { start: number }): Step<null> {
790
751
  const space = spaceOf(request);
791
- if (!patchesOverlapping(db, request.start, request.start, space).some((row) => row.start === request.start)) throw new KnowledgeError("not-found", `No patch starts at ${hex(request.start)}.`);
752
+ if (!patchesOverlapping(db, request.start, request.start, space).some((row) => row.start === request.start)) throw new Refusal("not-found", `No patch starts at ${formatC64Address(request.start)}.`);
792
753
  change.close("patches", "space = ? AND start_address = ?", space, request.start);
793
754
  return { current: null };
794
755
  }
@@ -796,21 +757,21 @@ function unpatchStep(db: DatabaseSync, change: Change, _origin: Origin, request:
796
757
  type MacroRequest = InSpace & { name: string; params?: string[]; body: string; comment?: string };
797
758
 
798
759
  function checkMacroName(name: unknown, what = "A macro name"): void {
799
- if (typeof name !== "string" || !MACRO_NAME.test(name)) throw new KnowledgeError("invalid-input", `${what} is a letter or underscore, then at most 31 letters, digits or underscores.`);
760
+ if (typeof name !== "string" || !MACRO_NAME.test(name)) throw new Refusal("invalid-input", `${what} is a letter or underscore, then at most 31 letters, digits or underscores.`);
800
761
  }
801
762
 
802
763
  function checkMacro(request: MacroRequest): void {
803
764
  checkSpace(request);
804
765
  checkMacroName(request.name);
805
766
  const params = request.params ?? [];
806
- if (!Array.isArray(params) || params.length > MAX_PARAMS) throw new KnowledgeError("invalid-input", `A macro has 0 to ${MAX_PARAMS} params.`);
767
+ if (!Array.isArray(params) || params.length > MAX_PARAMS) throw new Refusal("invalid-input", `A macro has 0 to ${MAX_PARAMS} params.`);
807
768
  for (const param of params) {
808
769
  if (typeof param !== "string" || !MACRO_PARAM.test(param)) {
809
- throw new KnowledgeError("invalid-input", "A macro param is a lower-case letter or underscore, then lower-case letters, digits or underscores, without a dot. The body names it with a dot: .dst.");
770
+ throw new Refusal("invalid-input", "A macro param is a lower-case letter or underscore, then lower-case letters, digits or underscores, without a dot. The body names it with a dot: .dst.");
810
771
  }
811
772
  }
812
- if (new Set(params).size !== params.length) throw new KnowledgeError("invalid-input", "The params of a macro have different names.");
813
- if (typeof request.body !== "string" || request.body.trim().length === 0 || request.body.length > MAX_CODE) throw new KnowledgeError("invalid-input", `A macro body has 1 to ${MAX_CODE} characters.`);
773
+ if (new Set(params).size !== params.length) throw new Refusal("invalid-input", "The params of a macro have different names.");
774
+ if (typeof request.body !== "string" || request.body.trim().length === 0 || request.body.length > MAX_CODE) throw new Refusal("invalid-input", `A macro body has 1 to ${MAX_CODE} characters.`);
814
775
  checkBraces(request.body, "The macro body");
815
776
  checkCommentText(request.comment, "A macro comment");
816
777
  }
@@ -825,9 +786,9 @@ function macroStep(db: DatabaseSync, change: Change, origin: Origin, request: Ma
825
786
  if (existing !== undefined && existing.params.length !== params.length) {
826
787
  const uses = listMacroUses(db, undefined, request.name, undefined, space);
827
788
  if (uses.length > 0) {
828
- throw new KnowledgeError(
789
+ throw new Refusal(
829
790
  "conflict",
830
- `The macro ${request.name} has ${existing.params.length} params, and its uses at ${uses.map((row) => hex(row.start)).join(", ")} give that many args. Remove the uses first (unuse), or keep the number of params.`,
791
+ `The macro ${request.name} has ${existing.params.length} params, and its uses at ${uses.map((row) => formatC64Address(row.start)).join(", ")} give that many args. Remove the uses first (unuse), or keep the number of params.`,
831
792
  );
832
793
  }
833
794
  }
@@ -839,9 +800,9 @@ function macroStep(db: DatabaseSync, change: Change, origin: Origin, request: Ma
839
800
 
840
801
  function unmacroStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { name: string }): Step<null> {
841
802
  const space = spaceOf(request);
842
- if (macroNamed(db, request.name, space) === undefined) throw new KnowledgeError("not-found", `There is no macro ${request.name}.`);
803
+ if (macroNamed(db, request.name, space) === undefined) throw new Refusal("not-found", `There is no macro ${request.name}.`);
843
804
  const uses = listMacroUses(db, undefined, request.name, undefined, space);
844
- if (uses.length > 0) throw new KnowledgeError("conflict", `The macro ${request.name} has uses at ${uses.map((row) => hex(row.start)).join(", ")}. Remove them first (unuse).`);
805
+ if (uses.length > 0) throw new Refusal("conflict", `The macro ${request.name} has uses at ${uses.map((row) => formatC64Address(row.start)).join(", ")}. Remove them first (unuse).`);
845
806
  change.close("macros", "space = ? AND name = ?", space, request.name);
846
807
  return { current: null };
847
808
  }
@@ -852,15 +813,10 @@ function checkMacroUse(request: MacroUseRequest): void {
852
813
  checkRange(request);
853
814
  checkMacroName(request.macro, "The macro of a use");
854
815
  const args = request.args ?? [];
855
- if (!Array.isArray(args) || args.length > MAX_PARAMS) throw new KnowledgeError("invalid-input", `A macro use has 0 to ${MAX_PARAMS} args.`);
816
+ if (!Array.isArray(args) || args.length > MAX_PARAMS) throw new Refusal("invalid-input", `A macro use has 0 to ${MAX_PARAMS} args.`);
856
817
  for (const arg of args) {
857
- if (typeof arg !== "string") throw new KnowledgeError("invalid-input", "Each arg of a macro use is expression text.");
858
- try {
859
- checkExpression(arg);
860
- } catch (error) {
861
- if (error instanceof ExpressionError) throw new KnowledgeError("invalid-input", error.message);
862
- throw error;
863
- }
818
+ if (typeof arg !== "string") throw new Refusal("invalid-input", "Each arg of a macro use is expression text.");
819
+ checkExpression(arg);
864
820
  }
865
821
  }
866
822
 
@@ -880,20 +836,20 @@ function renameInUses(db: DatabaseSync, change: Change, space: Space, from: stri
880
836
  function useStep(db: DatabaseSync, change: Change, context: ChangeContext & { image?: OperandImage }, request: MacroUseRequest): Step<MacroUseRow, MacroUseRow> {
881
837
  const space = spaceOf(request);
882
838
  const image = context.image;
883
- const range = `${hex(request.start)}-${hex(request.end)}`;
884
- if (image === undefined) throw new KnowledgeError("invalid-input", "A macro use is checked against the image, and no image was given.");
839
+ const range = `${formatC64Address(request.start)}-${formatC64Address(request.end)}`;
840
+ if (image === undefined) throw new Refusal("invalid-input", "A macro use is checked against the image, and no image was given.");
885
841
  const macro = macroNamed(db, request.macro, space);
886
- if (macro === undefined) throw new KnowledgeError("not-found", `There is no macro ${request.macro}. Write it first (macro).`);
842
+ if (macro === undefined) throw new Refusal("not-found", `There is no macro ${request.macro}. Write it first (macro).`);
887
843
  const args = (request.args ?? []).map((arg) => arg.trim());
888
844
  if (args.length !== macro.params.length) {
889
- throw new KnowledgeError("invalid-input", `The macro ${macro.name} has ${macro.params.length} params (${listText(macro.params)}), and the use gives ${args.length} args.`);
845
+ throw new Refusal("invalid-input", `The macro ${macro.name} has ${macro.params.length} params (${listText(macro.params)}), and the use gives ${args.length} args.`);
890
846
  }
891
847
  const resolve = (name: string) => symbolNamed(db, name, space)?.address ?? (space === DEFAULT_SPACE ? image.platform.get(name) : undefined);
892
848
  for (const arg of args) {
893
849
  try {
894
850
  evaluate(arg, resolve);
895
851
  } catch (error) {
896
- if (error instanceof ExpressionError) throw new KnowledgeError("invalid-input", `${range}: ${error.message}`);
852
+ if (error instanceof Refusal) throw new Refusal(error.code, `${range}: ${error.message}`);
897
853
  throw error;
898
854
  }
899
855
  }
@@ -901,15 +857,15 @@ function useStep(db: DatabaseSync, change: Change, context: ChangeContext & { im
901
857
  let next = request.start;
902
858
  while (next <= request.end) next += instructionLength(image.byteAt(next)!);
903
859
  if (next !== request.end + 1) {
904
- throw new KnowledgeError("invalid-input", `A macro use is a run of whole instructions, but the instructions from ${hex(request.start)} end at ${hex(next - 1)}, not at ${hex(request.end)}.`);
860
+ throw new Refusal("invalid-input", `A macro use is a run of whole instructions, but the instructions from ${formatC64Address(request.start)} end at ${formatC64Address(next - 1)}, not at ${formatC64Address(request.end)}.`);
905
861
  }
906
862
  const [patch] = patchesOverlapping(db, request.start, request.end, space);
907
863
  if (patch !== undefined) {
908
- throw new KnowledgeError("conflict", `The macro use ${range} overlaps the patch of ${patch.switch} at ${hex(patch.start)}-${hex(patch.end)}. Change or remove that patch first (unpatch).`);
864
+ throw new Refusal("conflict", `The macro use ${range} overlaps the patch of ${patch.switch} at ${formatC64Address(patch.start)}-${formatC64Address(patch.end)}. Change or remove that patch first (unpatch).`);
909
865
  }
910
866
  const other = macroUsesOverlapping(db, request.start, request.end, space).find((row) => row.start !== request.start);
911
867
  if (other !== undefined) {
912
- throw new KnowledgeError("conflict", `The macro use ${range} overlaps the use of ${other.macro} at ${hex(other.start)}-${hex(other.end)}. Remove that use first (unuse).`);
868
+ throw new Refusal("conflict", `The macro use ${range} overlaps the use of ${other.macro} at ${formatC64Address(other.start)}-${formatC64Address(other.end)}. Remove that use first (unuse).`);
913
869
  }
914
870
  const existing = macroUsesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start);
915
871
  if (existing !== undefined && existing.end === request.end && existing.macro === request.macro && listText(existing.args) === listText(args)) return { current: existing };
@@ -922,7 +878,7 @@ function useStep(db: DatabaseSync, change: Change, context: ChangeContext & { im
922
878
 
923
879
  function unuseStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { start: number }): Step<null> {
924
880
  const space = spaceOf(request);
925
- if (!macroUsesOverlapping(db, request.start, request.start, space).some((row) => row.start === request.start)) throw new KnowledgeError("not-found", `No macro use starts at ${hex(request.start)}.`);
881
+ if (!macroUsesOverlapping(db, request.start, request.start, space).some((row) => row.start === request.start)) throw new Refusal("not-found", `No macro use starts at ${formatC64Address(request.start)}.`);
926
882
  change.close("macro_uses", "space = ? AND start_address = ?", space, request.start);
927
883
  return { current: null };
928
884
  }
@@ -1072,7 +1028,7 @@ export type BatchStep = Step<
1072
1028
  * or none when one is refused. Every write is checked before anything opens.
1073
1029
  */
1074
1030
  export function applyBatch(db: DatabaseSync, context: WriteContext, operations: BatchOperation[]): { revision: string | null; steps: BatchStep[] } {
1075
- if (operations.length === 0 || operations.length > MAX_BATCH) throw new KnowledgeError("invalid-input", `A batch has 1 to ${MAX_BATCH} writes.`);
1031
+ if (operations.length === 0 || operations.length > MAX_BATCH) throw new Refusal("invalid-input", `A batch has 1 to ${MAX_BATCH} writes.`);
1076
1032
  operations.forEach((operation, index) => {
1077
1033
  try {
1078
1034
  switch (operation.op) {
@@ -1116,13 +1072,13 @@ export function applyBatch(db: DatabaseSync, context: WriteContext, operations:
1116
1072
  case "use":
1117
1073
  return checkMacroUse(operation);
1118
1074
  default:
1119
- throw new KnowledgeError(
1075
+ throw new Refusal(
1120
1076
  "invalid-input",
1121
1077
  `op must be one of rename, remove-symbol, classify, unclassify, comment, uncomment, reference, unreference, module, unmodule, operand, unoperand, patch, unpatch, macro, unmacro, use, unuse.`,
1122
1078
  );
1123
1079
  }
1124
1080
  } catch (error) {
1125
- if (error instanceof KnowledgeError) throw new KnowledgeError(error.code, `Write ${index + 1}: ${error.message} Nothing was written.`);
1081
+ if (error instanceof Refusal) throw new Refusal(error.code, `Write ${index + 1}: ${error.message} Nothing was written.`);
1126
1082
  throw error;
1127
1083
  }
1128
1084
  });
@@ -1168,7 +1124,7 @@ export function applyBatch(db: DatabaseSync, context: WriteContext, operations:
1168
1124
  return unuseStep(db, change, context.origin, operation);
1169
1125
  }
1170
1126
  } catch (error) {
1171
- if (error instanceof KnowledgeError) throw new KnowledgeError(error.code, `Write ${index + 1}: ${error.message} Nothing was written.`);
1127
+ if (error instanceof Refusal) throw new Refusal(error.code, `Write ${index + 1}: ${error.message} Nothing was written.`);
1172
1128
  throw error;
1173
1129
  }
1174
1130
  }),
@@ -1303,7 +1259,7 @@ export function mergeConflicts(db: DatabaseSync, context: WriteContext, platform
1303
1259
  return write(db, context, "merge", (change) => {
1304
1260
  const choices: MergeChoice[] = [];
1305
1261
  for (let found = conflicts(db); found.length > 0; found = conflicts(db)) {
1306
- if (choices.length >= MAX_CHOICES) throw new KnowledgeError("invalid-database", `The merge made ${MAX_CHOICES} choices and still finds conflicts. Nothing was written.`);
1262
+ if (choices.length >= MAX_CHOICES) throw new Refusal("invalid-database", `The merge made ${MAX_CHOICES} choices and still finds conflicts. Nothing was written.`);
1307
1263
  const conflict = found[0]!;
1308
1264
  const newest = conflict.rows.at(-1)!;
1309
1265
  const older = conflict.rows.slice(0, -1);