@henols/c64-re-tools 1.1.0 → 2.0.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +154 -37
- package/distribution/package.ts +21 -0
- package/distribution/plugin.ts +122 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +76 -0
- package/skills/c64-assembler/scripts/assemble.ts +110 -0
- package/skills/c64-assembler/scripts/compare.ts +84 -0
- package/skills/c64-basic/SKILL.md +66 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +93 -0
- package/skills/c64-disk/scripts/containers.ts +124 -0
- package/skills/c64-disk/scripts/disk.ts +120 -0
- package/skills/c64-emulator/SKILL.md +134 -0
- package/skills/c64-knowledge/SKILL.md +166 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +492 -0
- package/skills/c64-memory-map/SKILL.md +79 -0
- package/skills/c64-memory-map/scripts/memmap.ts +92 -0
- package/skills/c64-provenance/SKILL.md +110 -0
- package/skills/c64-provenance/scripts/compare.ts +152 -0
- package/skills/c64-provenance/scripts/provenance.ts +334 -0
- package/skills/c64-provenance/scripts/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/separate.ts +140 -0
- package/skills/c64-reverse-engineering/SKILL.md +144 -0
- package/skills/c64-source/SKILL.md +126 -0
- package/skills/c64-source/scripts/generate.ts +490 -0
- package/skills/c64-source/scripts/loader.ts +571 -0
- package/skills/c64-source/scripts/opcodes.ts +61 -0
- package/skills/c64-source/scripts/source.ts +595 -0
- package/skills/c64-static-analysis/SKILL.md +119 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +275 -0
- package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
- package/skills/c64-static-analysis/scripts/findings.ts +75 -0
- package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
- package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
- package/skills/c64-testing/SKILL.md +103 -0
- package/skills/c64-testing/references/scenario-format.md +116 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/run.ts +189 -0
- package/skills/c64-testing/scripts/scenario.ts +493 -0
- package/skills/c64-testing/scripts/test.ts +65 -0
- package/skills/c64-unpacker/SKILL.md +135 -0
- package/skills/c64-unpacker/scripts/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/stages.ts +130 -0
- package/skills/c64-unpacker/scripts/unpack.ts +683 -0
- package/src/c64.ts +411 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +125 -0
- package/src/cli/status.ts +61 -0
- package/src/host/main.ts +134 -0
- package/src/host/server.ts +392 -0
- package/src/host/tools/c1541.ts +284 -0
- package/src/host/tools/index.ts +35 -0
- package/src/host/tools/petcat.ts +77 -0
- package/src/host/tools/status.ts +47 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +804 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +118 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +413 -0
- package/src/host/vice/screen.ts +184 -0
- package/src/host/vice/session.ts +1426 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +241 -0
- package/src/host-client/coverage.ts +63 -0
- package/src/host-client/steps.ts +170 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +126 -0
- package/src/host-client/vice-session.ts +316 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/history.ts +163 -0
- package/src/knowledge/import.ts +391 -0
- package/src/knowledge/read.ts +342 -0
- package/src/knowledge/schema.ts +229 -0
- package/src/knowledge/write.ts +663 -0
- package/src/mcp/main.ts +134 -0
- package/src/mcp/output.ts +32 -0
- package/src/mcp/server.ts +316 -0
- package/src/mcp/tools/debug.ts +327 -0
- package/src/mcp/tools/execution.ts +117 -0
- package/src/mcp/tools/input.ts +72 -0
- package/src/mcp/tools/machine.ts +100 -0
- package/src/mcp/tools/media.ts +129 -0
- package/src/mcp/tools/memory.ts +250 -0
- package/src/mcp/tools/video.ts +271 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +179 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +173 -0
- package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
- package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
- package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
- package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
- package/src/native/ghidra/scripts/C64Export.java +274 -0
- package/src/native/ghidra/scripts/C64Prepare.java +60 -0
- package/src/native/local.ts +34 -0
- package/src/native/processes.ts +350 -0
- package/src/native/run.ts +172 -0
- package/src/native/staging.ts +83 -0
- package/src/native/status.ts +113 -0
- package/src/native/types.ts +257 -0
- package/src/native/watchdog.ts +78 -0
- package/src/package.ts +21 -0
- package/src/platform.ts +441 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +259 -0
- package/src/protocol/tools.ts +266 -0
- package/src/protocol/vice.ts +1348 -0
- package/src/script.ts +60 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +194 -0
- package/THIRD-PARTY-NOTICES.md +0 -26
- package/bin/cli.mjs +0 -283
- package/skills/acme-build/SKILL.md +0 -260
- package/skills/acme-build/scripts/acme.mjs +0 -341
- package/skills/acme-build/template.a +0 -53
- package/skills/c64-disk-access/SKILL.md +0 -159
- package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
- package/skills/c64-memory-mapping/SKILL.md +0 -631
- package/skills/c64-memory-mapping/memmap.json +0 -8800
- package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
- package/skills/c64-petcat/SKILL.md +0 -87
- package/skills/c64-petcat/scripts/petcat.mjs +0 -221
- package/skills/c64-program-recon/SKILL.md +0 -706
- package/skills/c64-program-recon/references/control-flow.md +0 -180
- package/skills/c64-program-recon/references/graphics.md +0 -79
- package/skills/c64-program-recon/references/observation-hazards.md +0 -143
- package/skills/c64-program-recon/references/reconstruction.md +0 -133
- package/skills/c64-program-recon/references/sound-and-input.md +0 -74
- package/skills/c64-program-recon/references/tool-selection.md +0 -78
- package/skills/c64-program-recon/scripts/derive.mjs +0 -364
- package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
- package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
- package/skills/c64-provenance-diff/SKILL.md +0 -292
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
- package/skills/c64-ram-capture/RELEASES.json.example +0 -17
- package/skills/c64-ram-capture/SKILL.md +0 -474
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
- package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
- package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
- package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
- package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
- package/skills/c64-ram-capture/transients/README.md +0 -137
- package/skills/routine-queue-walker/SKILL.md +0 -394
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
- package/skills/vice-wedge-triage/SKILL.md +0 -269
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
// Semantic knowledge writes. Every accepted change is one
|
|
2
|
+
// revision in one transaction: superseded rows are closed, never deleted.
|
|
3
|
+
|
|
4
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
5
|
+
|
|
6
|
+
import { SYMBOL_NAME } from "../c64.ts";
|
|
7
|
+
import { isoCet } from "../time.ts";
|
|
8
|
+
import { KnowledgeError, transaction } from "./database.ts";
|
|
9
|
+
import {
|
|
10
|
+
COMMENT_COLUMNS,
|
|
11
|
+
commentsAt,
|
|
12
|
+
currentRevision,
|
|
13
|
+
MODULE_COLUMNS,
|
|
14
|
+
moduleAt,
|
|
15
|
+
moduleWithFile,
|
|
16
|
+
REFERENCE_COLUMNS,
|
|
17
|
+
referencesFrom,
|
|
18
|
+
REGION_COLUMNS,
|
|
19
|
+
regionsOverlapping,
|
|
20
|
+
SUPPRESSION_COLUMNS,
|
|
21
|
+
suppressionsOverlapping,
|
|
22
|
+
SYMBOL_COLUMNS,
|
|
23
|
+
symbolAt,
|
|
24
|
+
symbolNamed,
|
|
25
|
+
toComment,
|
|
26
|
+
toModule,
|
|
27
|
+
toReference,
|
|
28
|
+
toRegion,
|
|
29
|
+
toSuppression,
|
|
30
|
+
toSymbol,
|
|
31
|
+
type CommentRow,
|
|
32
|
+
type ModuleRow,
|
|
33
|
+
type ReferenceRow,
|
|
34
|
+
type RegionRow,
|
|
35
|
+
type SuppressionRow,
|
|
36
|
+
type SymbolRow,
|
|
37
|
+
} from "./read.ts";
|
|
38
|
+
import {
|
|
39
|
+
COMMENT_PLACEMENTS,
|
|
40
|
+
EVIDENCE_KINDS,
|
|
41
|
+
MAX_ELEMENT,
|
|
42
|
+
MODULE_FILE,
|
|
43
|
+
DEFAULT_SPACE,
|
|
44
|
+
GENERATED_FILES,
|
|
45
|
+
isGeneratedFile,
|
|
46
|
+
SPACE,
|
|
47
|
+
NO_ELEMENT_TYPES,
|
|
48
|
+
REFERENCE_KINDS,
|
|
49
|
+
REGION_TYPES,
|
|
50
|
+
SYMBOL_KINDS,
|
|
51
|
+
type CommentPlacement,
|
|
52
|
+
type EvidenceKind,
|
|
53
|
+
type Origin,
|
|
54
|
+
type ReferenceKind,
|
|
55
|
+
type RegionType,
|
|
56
|
+
type Space,
|
|
57
|
+
type SymbolKind,
|
|
58
|
+
} from "./schema.ts";
|
|
59
|
+
|
|
60
|
+
/** What a change rests on (D42): a coverage run, a test or another project file with its SHA-256, or a note. */
|
|
61
|
+
export interface Evidence {
|
|
62
|
+
kind: EvidenceKind;
|
|
63
|
+
ref: string;
|
|
64
|
+
sha256?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Who writes and why. Semantic writes come from the user or the LLM, never as an analyzer. */
|
|
68
|
+
export interface WriteContext {
|
|
69
|
+
origin: Extract<Origin, "user" | "llm">;
|
|
70
|
+
/** A short reason, kept with the revision for later review. */
|
|
71
|
+
description?: string;
|
|
72
|
+
/** The revision the caller read; the write is refused if knowledge changed since. */
|
|
73
|
+
expectedRevision?: number;
|
|
74
|
+
/** Kept with the revision when the write changes something. */
|
|
75
|
+
evidence?: Evidence[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The revision a write created, or null when nothing needed to change. */
|
|
79
|
+
export interface WriteResult<T, P = never> {
|
|
80
|
+
revision: number | null;
|
|
81
|
+
current: T;
|
|
82
|
+
/** The current rows of another origin that this write replaced, as they were before it. */
|
|
83
|
+
previous?: P;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { SYMBOL_NAME };
|
|
87
|
+
const MAX_COMMENT = 4000;
|
|
88
|
+
const MAX_EVIDENCE_REF = 1000;
|
|
89
|
+
|
|
90
|
+
export function checkAddress(value: number, what = "address"): void {
|
|
91
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffff) throw new KnowledgeError("invalid-input", `${what} must be from $0000 to $ffff.`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function checkOneOf<T extends string>(values: readonly T[], value: string, what: string): asserts value is T {
|
|
95
|
+
if (!(values as readonly string[]).includes(value)) throw new KnowledgeError("invalid-input", `${what} must be one of: ${values.join(", ")}.`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function hex(value: number): string {
|
|
99
|
+
return `$${value.toString(16).padStart(4, "0")}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface AddressRange {
|
|
103
|
+
start: number;
|
|
104
|
+
end: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Parts of `range` that no range in `holes` covers. */
|
|
108
|
+
export function subtract(range: AddressRange, holes: AddressRange[]): AddressRange[] {
|
|
109
|
+
let pieces = [range];
|
|
110
|
+
for (const hole of holes) {
|
|
111
|
+
pieces = pieces.flatMap((piece) => {
|
|
112
|
+
if (hole.end < piece.start || hole.start > piece.end) return [piece];
|
|
113
|
+
const left = hole.start > piece.start ? [{ start: piece.start, end: hole.start - 1 }] : [];
|
|
114
|
+
const right = hole.end < piece.end ? [{ start: hole.end + 1, end: piece.end }] : [];
|
|
115
|
+
return [...left, ...right];
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return pieces;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** What a revision records about its writer. Analyzer imports may add tool details. */
|
|
122
|
+
export interface ChangeContext {
|
|
123
|
+
origin: Origin;
|
|
124
|
+
description?: string;
|
|
125
|
+
expectedRevision?: number;
|
|
126
|
+
inputHash?: string;
|
|
127
|
+
toolVersion?: string;
|
|
128
|
+
evidence?: Evidence[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* One write: refuses a stale expected revision, then creates the revision row
|
|
133
|
+
* only when the first actual change happens, so a no-op leaves no revision.
|
|
134
|
+
*/
|
|
135
|
+
export class Change {
|
|
136
|
+
revision: number | null = null;
|
|
137
|
+
readonly #db: DatabaseSync;
|
|
138
|
+
readonly #context: ChangeContext;
|
|
139
|
+
readonly #operation: string;
|
|
140
|
+
|
|
141
|
+
constructor(db: DatabaseSync, context: ChangeContext, operation: string) {
|
|
142
|
+
this.#db = db;
|
|
143
|
+
this.#context = context;
|
|
144
|
+
this.#operation = operation;
|
|
145
|
+
const current = currentRevision(db);
|
|
146
|
+
if (context.expectedRevision !== undefined && context.expectedRevision !== current) {
|
|
147
|
+
throw new KnowledgeError(
|
|
148
|
+
"stale-revision",
|
|
149
|
+
`Knowledge changed since revision ${context.expectedRevision}; it is now at revision ${current}. Read it again and decide again.`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The id of this write's revision, created on first use with its evidence. */
|
|
155
|
+
id(): number {
|
|
156
|
+
if (this.revision !== null) return this.revision;
|
|
157
|
+
const created = this.#db
|
|
158
|
+
.prepare("INSERT INTO revisions (created_at, origin, operation, input_hash, tool_version, description) VALUES (?, ?, ?, ?, ?, ?) RETURNING id")
|
|
159
|
+
.get(
|
|
160
|
+
isoCet(new Date()),
|
|
161
|
+
this.#context.origin,
|
|
162
|
+
this.#operation,
|
|
163
|
+
this.#context.inputHash ?? null,
|
|
164
|
+
this.#context.toolVersion ?? null,
|
|
165
|
+
this.#context.description ?? null,
|
|
166
|
+
) as { id: number };
|
|
167
|
+
this.revision = created.id;
|
|
168
|
+
this.#db.prepare("UPDATE meta SET value = ? WHERE key = 'current_revision'").run(String(created.id));
|
|
169
|
+
const evidence = this.#db.prepare("INSERT INTO evidence (revision, kind, ref, sha256) VALUES (?, ?, ?, ?)");
|
|
170
|
+
for (const item of this.#context.evidence ?? []) evidence.run(created.id, item.kind, item.ref, item.sha256 ?? null);
|
|
171
|
+
return created.id;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Closes the current rows of `table` that `where` selects. */
|
|
175
|
+
close(table: string, where: string, ...values: Array<number | string>): void {
|
|
176
|
+
this.#db.prepare(`UPDATE ${table} SET valid_to_revision = ? WHERE valid_to_revision IS NULL AND ${where}`).run(this.id(), ...values);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
insertSymbol(row: Omit<SymbolRow, "revision">): void {
|
|
180
|
+
this.#db.prepare("INSERT INTO symbols (space, address, name, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?)").run(row.space, row.address, row.name, row.kind, row.origin, this.id());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
insertRegion(row: Omit<RegionRow, "revision">): void {
|
|
184
|
+
this.#db
|
|
185
|
+
.prepare("INSERT INTO regions (space, start_address, end_address, type, element, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
186
|
+
.run(row.space, row.start, row.end, row.type, row.element ?? null, row.origin, this.id());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
insertComment(row: Omit<CommentRow, "revision">): void {
|
|
190
|
+
this.#db
|
|
191
|
+
.prepare("INSERT INTO comments (space, address, placement, text, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?)")
|
|
192
|
+
.run(row.space, row.address, row.placement, row.text, row.origin, this.id());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
insertReference(row: Omit<ReferenceRow, "revision">): void {
|
|
196
|
+
this.#db
|
|
197
|
+
.prepare('INSERT INTO "references" (space, from_address, to_address, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?)')
|
|
198
|
+
.run(row.space, row.from, row.to, row.kind, row.origin, this.id());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
insertModule(row: Omit<ModuleRow, "revision">): void {
|
|
202
|
+
this.#db
|
|
203
|
+
.prepare("INSERT INTO modules (space, start_address, file, title, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?)")
|
|
204
|
+
.run(row.space, row.start, row.file, row.title ?? null, row.origin, this.id());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
insertSuppression(row: Omit<SuppressionRow, "revision">): void {
|
|
208
|
+
this.#db
|
|
209
|
+
.prepare("INSERT INTO suppressions (space, entity, start_address, end_address, to_address, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
|
210
|
+
.run(row.space, row.entity, row.start, row.end, row.to ?? null, row.kind ?? null, row.origin, this.id());
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Closes the current region suppressions over [start, end] and keeps their parts outside it: a user/LLM region there decides now. */
|
|
215
|
+
function liftRegionSuppressions(db: DatabaseSync, change: Change, start: number, end: number, space: Space): void {
|
|
216
|
+
const overlapping = suppressionsOverlapping(db, "region", start, end, space);
|
|
217
|
+
if (overlapping.length === 0) return;
|
|
218
|
+
change.close("suppressions", "space = ? AND entity = 'region' AND start_address <= ? AND end_address >= ?", space, end, start);
|
|
219
|
+
for (const row of overlapping) {
|
|
220
|
+
if (row.start < start) change.insertSuppression({ ...row, end: start - 1 });
|
|
221
|
+
if (row.end > end) change.insertSuppression({ ...row, start: end + 1 });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function checkEvidence(evidence: Evidence[] | undefined): void {
|
|
226
|
+
for (const item of evidence ?? []) {
|
|
227
|
+
checkOneOf(EVIDENCE_KINDS, item.kind, "evidence kind");
|
|
228
|
+
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.`);
|
|
229
|
+
if (item.sha256 !== undefined && !/^[0-9a-f]{64}$/.test(item.sha256)) throw new KnowledgeError("invalid-input", "evidence sha256 must be 64 lowercase hex digits.");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Runs semantic writes in one transaction, as one revision. */
|
|
234
|
+
function write<T>(db: DatabaseSync, context: WriteContext, operation: string, work: (change: Change) => T): { revision: number | null; current: T } {
|
|
235
|
+
if (context.origin !== "user" && context.origin !== "llm") {
|
|
236
|
+
throw new KnowledgeError("invalid-input", "A semantic write comes from the user or the LLM.");
|
|
237
|
+
}
|
|
238
|
+
checkEvidence(context.evidence);
|
|
239
|
+
return transaction(db, () => {
|
|
240
|
+
const change = new Change(db, context, operation);
|
|
241
|
+
const current = work(change);
|
|
242
|
+
return { revision: change.revision, current };
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** One step of a write inside a change: its current value, and the rows of another origin it replaced. */
|
|
247
|
+
interface Step<T, P = never> {
|
|
248
|
+
current: T;
|
|
249
|
+
previous?: P;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const result = <T, P>(written: { revision: number | null; current: Step<T, P> }): WriteResult<T, P> =>
|
|
253
|
+
written.current.previous === undefined ? { revision: written.revision, current: written.current.current } : { revision: written.revision, current: written.current.current, previous: written.current.previous };
|
|
254
|
+
|
|
255
|
+
// Each write has a check, which refuses bad input before anything opens, and a
|
|
256
|
+
// step, which runs inside a change. A single write and a batch share both (D42).
|
|
257
|
+
|
|
258
|
+
/** Every request names its space (D53); without one it is the C64. */
|
|
259
|
+
type InSpace = { space?: Space };
|
|
260
|
+
type SymbolRequest = InSpace & { address: number; name: string; kind?: SymbolKind };
|
|
261
|
+
type RegionRequest = InSpace & { start: number; end: number; type: RegionType; element?: number };
|
|
262
|
+
type RangeRequest = InSpace & { start: number; end: number };
|
|
263
|
+
type CommentRequest = InSpace & { address: number; placement: CommentPlacement; text: string };
|
|
264
|
+
type PlacementRequest = InSpace & { address: number; placement: CommentPlacement };
|
|
265
|
+
type ReferenceRequest = InSpace & { from: number; to: number; kind: ReferenceKind };
|
|
266
|
+
type AddressRequest = InSpace & { address: number };
|
|
267
|
+
|
|
268
|
+
const spaceOf = (request: InSpace): Space => request.space ?? DEFAULT_SPACE;
|
|
269
|
+
|
|
270
|
+
export function checkSpace(request: InSpace): void {
|
|
271
|
+
if (request.space !== undefined && !SPACE.test(request.space)) throw new KnowledgeError("invalid-input", "space must be c64, drive8, or bank0 to bank255.");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function checkSymbol(request: SymbolRequest): void {
|
|
275
|
+
checkSpace(request);
|
|
276
|
+
checkAddress(request.address);
|
|
277
|
+
if (!SYMBOL_NAME.test(request.name)) {
|
|
278
|
+
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.");
|
|
279
|
+
}
|
|
280
|
+
if (request.kind !== undefined) checkOneOf(SYMBOL_KINDS, request.kind, "kind");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function renameStep(db: DatabaseSync, change: Change, origin: Origin, request: SymbolRequest): Step<SymbolRow, SymbolRow> {
|
|
284
|
+
const space = spaceOf(request);
|
|
285
|
+
const existing = symbolAt(db, request.address, space);
|
|
286
|
+
const kind = request.kind ?? existing?.kind ?? "label";
|
|
287
|
+
if (existing !== undefined && existing.name === request.name && existing.kind === kind) return { current: existing };
|
|
288
|
+
const owner = symbolNamed(db, request.name, space);
|
|
289
|
+
if (owner !== undefined && owner.address !== request.address) {
|
|
290
|
+
throw new KnowledgeError("conflict", `The name ${request.name} already belongs to ${hex(owner.address)}. Rename or remove that symbol first.`);
|
|
291
|
+
}
|
|
292
|
+
if (existing !== undefined) change.close("symbols", "space = ? AND address = ?", space, request.address);
|
|
293
|
+
change.insertSymbol({ space, address: request.address, name: request.name, kind, origin });
|
|
294
|
+
change.close("suppressions", "space = ? AND entity = 'symbol' AND start_address = ?", space, request.address);
|
|
295
|
+
const current = symbolAt(db, request.address, space)!;
|
|
296
|
+
return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function removeSymbolStep(db: DatabaseSync, change: Change, origin: Origin, request: AddressRequest): Step<null> {
|
|
300
|
+
const space = spaceOf(request);
|
|
301
|
+
if (symbolAt(db, request.address, space) === undefined) throw new KnowledgeError("not-found", `There is no symbol at ${hex(request.address)}.`);
|
|
302
|
+
change.close("symbols", "space = ? AND address = ?", space, request.address);
|
|
303
|
+
change.insertSuppression({ space, entity: "symbol", start: request.address, end: request.address, origin: origin as SuppressionRow["origin"] });
|
|
304
|
+
return { current: null };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function checkRange(request: RangeRequest): void {
|
|
308
|
+
checkSpace(request);
|
|
309
|
+
checkAddress(request.start, "start");
|
|
310
|
+
checkAddress(request.end, "end");
|
|
311
|
+
if (request.end < request.start) throw new KnowledgeError("invalid-input", "end must not be before start.");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function checkRegion(request: RegionRequest): void {
|
|
315
|
+
checkRange(request);
|
|
316
|
+
checkOneOf(REGION_TYPES, request.type, "type");
|
|
317
|
+
if (request.element === undefined) return;
|
|
318
|
+
if (NO_ELEMENT_TYPES.includes(request.type)) throw new KnowledgeError("invalid-input", `A ${request.type} region has no element size.`);
|
|
319
|
+
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}.`);
|
|
320
|
+
const length = request.end - request.start + 1;
|
|
321
|
+
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}.`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** The element size of a piece of a region: kept when the piece still holds whole elements. */
|
|
325
|
+
function elementOf(piece: AddressRange, element: number | undefined): { element?: number } {
|
|
326
|
+
return element !== undefined && (piece.end - piece.start + 1) % element === 0 ? { element } : {};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Closes the current regions over [start, end] and keeps their parts outside it. */
|
|
330
|
+
export function clearRange(db: DatabaseSync, change: Change, start: number, end: number, space: Space = DEFAULT_SPACE): void {
|
|
331
|
+
const overlapping = regionsOverlapping(db, start, end, space);
|
|
332
|
+
if (overlapping.length === 0) return;
|
|
333
|
+
change.close("regions", "space = ? AND start_address <= ? AND end_address >= ?", space, end, start);
|
|
334
|
+
for (const region of overlapping) {
|
|
335
|
+
const left = { start: region.start, end: start - 1 };
|
|
336
|
+
const right = { start: end + 1, end: region.end };
|
|
337
|
+
if (region.start < start) change.insertRegion({ space, ...left, type: region.type, ...elementOf(left, region.element), origin: region.origin });
|
|
338
|
+
if (region.end > end) change.insertRegion({ space, ...right, type: region.type, ...elementOf(right, region.element), origin: region.origin });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function classifyStep(db: DatabaseSync, change: Change, origin: Origin, request: RegionRequest): Step<RegionRow[], RegionRow[]> {
|
|
343
|
+
const space = spaceOf(request);
|
|
344
|
+
const overlapping = regionsOverlapping(db, request.start, request.end, space);
|
|
345
|
+
const [only] = overlapping;
|
|
346
|
+
const same = overlapping.length === 1 && only!.start === request.start && only!.end === request.end && only!.type === request.type && only!.element === request.element;
|
|
347
|
+
if (same) return { current: overlapping };
|
|
348
|
+
clearRange(db, change, request.start, request.end, space);
|
|
349
|
+
change.insertRegion({ space, start: request.start, end: request.end, type: request.type, ...(request.element === undefined ? {} : { element: request.element }), origin });
|
|
350
|
+
liftRegionSuppressions(db, change, request.start, request.end, space);
|
|
351
|
+
const previous = overlapping.filter((row) => row.origin !== origin);
|
|
352
|
+
const current = regionsOverlapping(db, request.start, request.end, space);
|
|
353
|
+
return previous.length === 0 ? { current } : { current, previous };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function unclassifyStep(db: DatabaseSync, change: Change, origin: Origin, request: RangeRequest): Step<null> {
|
|
357
|
+
const space = spaceOf(request);
|
|
358
|
+
if (regionsOverlapping(db, request.start, request.end, space).length === 0) {
|
|
359
|
+
throw new KnowledgeError("not-found", `No region covers ${hex(request.start)}-${hex(request.end)}.`);
|
|
360
|
+
}
|
|
361
|
+
clearRange(db, change, request.start, request.end, space);
|
|
362
|
+
// Suppress only the parts that no region suppression holds yet, so suppressions never overlap.
|
|
363
|
+
for (const piece of subtract({ start: request.start, end: request.end }, suppressionsOverlapping(db, "region", request.start, request.end, space))) {
|
|
364
|
+
change.insertSuppression({ space, entity: "region", ...piece, origin: origin as SuppressionRow["origin"] });
|
|
365
|
+
}
|
|
366
|
+
return { current: null };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function checkComment(request: CommentRequest): string {
|
|
370
|
+
checkSpace(request);
|
|
371
|
+
checkAddress(request.address);
|
|
372
|
+
checkOneOf(COMMENT_PLACEMENTS, request.placement, "placement");
|
|
373
|
+
const text = request.text.trim();
|
|
374
|
+
if (text.length === 0 || text.length > MAX_COMMENT) throw new KnowledgeError("invalid-input", `A comment has 1 to ${MAX_COMMENT} characters.`);
|
|
375
|
+
return text;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function commentStep(db: DatabaseSync, change: Change, origin: Origin, request: CommentRequest): Step<CommentRow> {
|
|
379
|
+
const space = spaceOf(request);
|
|
380
|
+
const text = request.text.trim();
|
|
381
|
+
const existing = commentsAt(db, request.address, space).find((comment) => comment.placement === request.placement);
|
|
382
|
+
if (existing?.text === text) return { current: existing };
|
|
383
|
+
if (existing !== undefined) change.close("comments", "space = ? AND address = ? AND placement = ?", space, request.address, request.placement);
|
|
384
|
+
change.insertComment({ space, address: request.address, placement: request.placement, text, origin });
|
|
385
|
+
return { current: commentsAt(db, request.address, space).find((comment) => comment.placement === request.placement)! };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function checkPlacement(request: PlacementRequest): void {
|
|
389
|
+
checkSpace(request);
|
|
390
|
+
checkAddress(request.address);
|
|
391
|
+
checkOneOf(COMMENT_PLACEMENTS, request.placement, "placement");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function uncommentStep(db: DatabaseSync, change: Change, _origin: Origin, request: PlacementRequest): Step<null> {
|
|
395
|
+
const space = spaceOf(request);
|
|
396
|
+
if (!commentsAt(db, request.address, space).some((comment) => comment.placement === request.placement)) {
|
|
397
|
+
throw new KnowledgeError("not-found", `There is no ${request.placement} comment at ${hex(request.address)}.`);
|
|
398
|
+
}
|
|
399
|
+
change.close("comments", "space = ? AND address = ? AND placement = ?", space, request.address, request.placement);
|
|
400
|
+
return { current: null };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function checkReference(request: ReferenceRequest): void {
|
|
404
|
+
checkSpace(request);
|
|
405
|
+
checkAddress(request.from, "from");
|
|
406
|
+
checkAddress(request.to, "to");
|
|
407
|
+
checkOneOf(REFERENCE_KINDS, request.kind, "kind");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function referenceStep(db: DatabaseSync, change: Change, origin: Origin, request: ReferenceRequest): Step<ReferenceRow> {
|
|
411
|
+
const space = spaceOf(request);
|
|
412
|
+
const find = () => referencesFrom(db, request.from, space).find((reference) => reference.to === request.to && reference.kind === request.kind);
|
|
413
|
+
const existing = find();
|
|
414
|
+
if (existing !== undefined) return { current: existing };
|
|
415
|
+
change.insertReference({ space, from: request.from, to: request.to, kind: request.kind, origin });
|
|
416
|
+
change.close("suppressions", "space = ? AND entity = 'reference' AND start_address = ? AND to_address = ? AND kind = ?", space, request.from, request.to, request.kind);
|
|
417
|
+
return { current: find()! };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function unreferenceStep(db: DatabaseSync, change: Change, origin: Origin, request: ReferenceRequest): Step<null> {
|
|
421
|
+
const space = spaceOf(request);
|
|
422
|
+
if (!referencesFrom(db, request.from, space).some((reference) => reference.to === request.to && reference.kind === request.kind)) {
|
|
423
|
+
throw new KnowledgeError("not-found", `There is no ${request.kind} reference from ${hex(request.from)} to ${hex(request.to)}.`);
|
|
424
|
+
}
|
|
425
|
+
change.close('"references"', "space = ? AND from_address = ? AND to_address = ? AND kind = ?", space, request.from, request.to, request.kind);
|
|
426
|
+
change.insertSuppression({ space, entity: "reference", start: request.from, end: request.from, to: request.to, kind: request.kind, origin: origin as SuppressionRow["origin"] });
|
|
427
|
+
return { current: null };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
type ModuleRequest = InSpace & { start: number; file: string; title?: string };
|
|
431
|
+
|
|
432
|
+
function checkModule(request: ModuleRequest): void {
|
|
433
|
+
checkSpace(request);
|
|
434
|
+
checkAddress(request.start, "start");
|
|
435
|
+
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.");
|
|
436
|
+
if (isGeneratedFile(request.file)) {
|
|
437
|
+
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.`);
|
|
438
|
+
}
|
|
439
|
+
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.`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function moduleStep(db: DatabaseSync, change: Change, origin: Origin, request: ModuleRequest): Step<ModuleRow> {
|
|
443
|
+
const space = spaceOf(request);
|
|
444
|
+
const existing = moduleAt(db, request.start, space);
|
|
445
|
+
// A module write without a title keeps the title it has (F-26).
|
|
446
|
+
const title = request.title === undefined ? existing?.title : request.title.trim();
|
|
447
|
+
if (existing !== undefined && existing.file === request.file && existing.title === title) return { current: existing };
|
|
448
|
+
const owner = moduleWithFile(db, request.file, space);
|
|
449
|
+
if (owner !== undefined && owner.start !== request.start) {
|
|
450
|
+
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.`);
|
|
451
|
+
}
|
|
452
|
+
if (existing !== undefined) change.close("modules", "space = ? AND start_address = ?", space, request.start);
|
|
453
|
+
change.insertModule({ space, start: request.start, file: request.file, ...(title === undefined ? {} : { title }), origin: origin as ModuleRow["origin"] });
|
|
454
|
+
return { current: moduleAt(db, request.start, space)! };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function unmoduleStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { start: number }): Step<null> {
|
|
458
|
+
const space = spaceOf(request);
|
|
459
|
+
if (moduleAt(db, request.start, space) === undefined) throw new KnowledgeError("not-found", `No module starts at ${hex(request.start)}.`);
|
|
460
|
+
change.close("modules", "space = ? AND start_address = ?", space, request.start);
|
|
461
|
+
return { current: null };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Names the symbol at an address (one current symbol per address, unique names). */
|
|
465
|
+
export function renameSymbol(db: DatabaseSync, context: WriteContext, request: SymbolRequest): WriteResult<SymbolRow, SymbolRow> {
|
|
466
|
+
checkSymbol(request);
|
|
467
|
+
return result(write(db, context, "rename-symbol", (change) => renameStep(db, change, context.origin, request)));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function removeSymbol(db: DatabaseSync, context: WriteContext, request: AddressRequest): WriteResult<null> {
|
|
471
|
+
checkSpace(request);
|
|
472
|
+
checkAddress(request.address);
|
|
473
|
+
return result(write(db, context, "remove-symbol", (change) => removeSymbolStep(db, change, context.origin, request)));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Classifies [start, end]; overlapping regions are split so current regions never overlap. */
|
|
477
|
+
export function classifyRegion(db: DatabaseSync, context: WriteContext, request: RegionRequest): WriteResult<RegionRow[], RegionRow[]> {
|
|
478
|
+
checkRegion(request);
|
|
479
|
+
return result(write(db, context, "classify-region", (change) => classifyStep(db, change, context.origin, request)));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Makes [start, end] unclassified again; parts of regions outside it stay. */
|
|
483
|
+
export function unclassifyRegion(db: DatabaseSync, context: WriteContext, request: RangeRequest): WriteResult<null> {
|
|
484
|
+
checkRange(request);
|
|
485
|
+
return result(write(db, context, "unclassify-region", (change) => unclassifyStep(db, change, context.origin, request)));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function setComment(db: DatabaseSync, context: WriteContext, request: CommentRequest): WriteResult<CommentRow> {
|
|
489
|
+
checkComment(request);
|
|
490
|
+
return result(write(db, context, "set-comment", (change) => commentStep(db, change, context.origin, request)));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function removeComment(db: DatabaseSync, context: WriteContext, request: PlacementRequest): WriteResult<null> {
|
|
494
|
+
checkPlacement(request);
|
|
495
|
+
return result(write(db, context, "remove-comment", (change) => uncommentStep(db, change, context.origin, request)));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function addReference(db: DatabaseSync, context: WriteContext, request: ReferenceRequest): WriteResult<ReferenceRow> {
|
|
499
|
+
checkReference(request);
|
|
500
|
+
return result(write(db, context, "add-reference", (change) => referenceStep(db, change, context.origin, request)));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function removeReference(db: DatabaseSync, context: WriteContext, request: ReferenceRequest): WriteResult<null> {
|
|
504
|
+
checkReference(request);
|
|
505
|
+
return result(write(db, context, "remove-reference", (change) => unreferenceStep(db, change, context.origin, request)));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** Starts a file of the generated source at an address (D48); one module per address and per file. */
|
|
509
|
+
export function setModule(db: DatabaseSync, context: WriteContext, request: ModuleRequest): WriteResult<ModuleRow> {
|
|
510
|
+
checkModule(request);
|
|
511
|
+
return result(write(db, context, "set-module", (change) => moduleStep(db, change, context.origin, request)));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function removeModule(db: DatabaseSync, context: WriteContext, request: InSpace & { start: number }): WriteResult<null> {
|
|
515
|
+
checkSpace(request);
|
|
516
|
+
checkAddress(request.start, "start");
|
|
517
|
+
return result(write(db, context, "remove-module", (change) => unmoduleStep(db, change, context.origin, request)));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** One write of a batch (D42), named as the command that does it alone. */
|
|
521
|
+
export type BatchOperation =
|
|
522
|
+
| ({ op: "rename" } & SymbolRequest)
|
|
523
|
+
| ({ op: "remove-symbol" } & AddressRequest)
|
|
524
|
+
| ({ op: "classify" } & RegionRequest)
|
|
525
|
+
| ({ op: "unclassify" } & RangeRequest)
|
|
526
|
+
| ({ op: "comment" } & CommentRequest)
|
|
527
|
+
| ({ op: "uncomment" } & PlacementRequest)
|
|
528
|
+
| ({ op: "reference" } & ReferenceRequest)
|
|
529
|
+
| ({ op: "unreference" } & ReferenceRequest)
|
|
530
|
+
| ({ op: "module" } & ModuleRequest)
|
|
531
|
+
| ({ op: "unmodule" } & InSpace & { start: number });
|
|
532
|
+
|
|
533
|
+
export const MAX_BATCH = 10_000;
|
|
534
|
+
|
|
535
|
+
/** The result of each write of a batch, in order. */
|
|
536
|
+
export type BatchStep = Step<SymbolRow | RegionRow[] | CommentRow | ReferenceRow | ModuleRow | null, SymbolRow | RegionRow[]>;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Applies many writes as one revision in one transaction (D42): all of them,
|
|
540
|
+
* or none when one is refused. Every write is checked before anything opens.
|
|
541
|
+
*/
|
|
542
|
+
export function applyBatch(db: DatabaseSync, context: WriteContext, operations: BatchOperation[]): { revision: number | null; steps: BatchStep[] } {
|
|
543
|
+
if (operations.length === 0 || operations.length > MAX_BATCH) throw new KnowledgeError("invalid-input", `A batch has 1 to ${MAX_BATCH} writes.`);
|
|
544
|
+
operations.forEach((operation, index) => {
|
|
545
|
+
try {
|
|
546
|
+
switch (operation.op) {
|
|
547
|
+
case "rename":
|
|
548
|
+
return checkSymbol(operation);
|
|
549
|
+
case "remove-symbol":
|
|
550
|
+
checkSpace(operation);
|
|
551
|
+
return checkAddress(operation.address);
|
|
552
|
+
case "classify":
|
|
553
|
+
return checkRegion(operation);
|
|
554
|
+
case "unclassify":
|
|
555
|
+
return checkRange(operation);
|
|
556
|
+
case "comment":
|
|
557
|
+
return void checkComment(operation);
|
|
558
|
+
case "uncomment":
|
|
559
|
+
return checkPlacement(operation);
|
|
560
|
+
case "reference":
|
|
561
|
+
case "unreference":
|
|
562
|
+
return checkReference(operation);
|
|
563
|
+
case "module":
|
|
564
|
+
return checkModule(operation);
|
|
565
|
+
case "unmodule":
|
|
566
|
+
checkSpace(operation);
|
|
567
|
+
return checkAddress(operation.start, "start");
|
|
568
|
+
default:
|
|
569
|
+
throw new KnowledgeError("invalid-input", `op must be one of rename, remove-symbol, classify, unclassify, comment, uncomment, reference, unreference, module, unmodule.`);
|
|
570
|
+
}
|
|
571
|
+
} catch (error) {
|
|
572
|
+
if (error instanceof KnowledgeError) throw new KnowledgeError(error.code, `Write ${index + 1}: ${error.message} Nothing was written.`);
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
const written = write(db, context, "apply", (change) =>
|
|
577
|
+
operations.map((operation, index): BatchStep => {
|
|
578
|
+
try {
|
|
579
|
+
switch (operation.op) {
|
|
580
|
+
case "rename":
|
|
581
|
+
return renameStep(db, change, context.origin, operation);
|
|
582
|
+
case "remove-symbol":
|
|
583
|
+
return removeSymbolStep(db, change, context.origin, operation);
|
|
584
|
+
case "classify":
|
|
585
|
+
return classifyStep(db, change, context.origin, operation);
|
|
586
|
+
case "unclassify":
|
|
587
|
+
return unclassifyStep(db, change, context.origin, operation);
|
|
588
|
+
case "comment":
|
|
589
|
+
return commentStep(db, change, context.origin, operation);
|
|
590
|
+
case "uncomment":
|
|
591
|
+
return uncommentStep(db, change, context.origin, operation);
|
|
592
|
+
case "reference":
|
|
593
|
+
return referenceStep(db, change, context.origin, operation);
|
|
594
|
+
case "unreference":
|
|
595
|
+
return unreferenceStep(db, change, context.origin, operation);
|
|
596
|
+
case "module":
|
|
597
|
+
return moduleStep(db, change, context.origin, operation);
|
|
598
|
+
case "unmodule":
|
|
599
|
+
return unmoduleStep(db, change, context.origin, operation);
|
|
600
|
+
}
|
|
601
|
+
} catch (error) {
|
|
602
|
+
if (error instanceof KnowledgeError) throw new KnowledgeError(error.code, `Write ${index + 1}: ${error.message} Nothing was written.`);
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
605
|
+
}),
|
|
606
|
+
);
|
|
607
|
+
return { revision: written.revision, steps: written.current };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** Row content without its revision, as a comparable key. */
|
|
611
|
+
const KEYS = {
|
|
612
|
+
symbols: { columns: SYMBOL_COLUMNS, map: toSymbol, key: (row: SymbolRow) => `${row.space}|${row.address}|${row.name}|${row.kind}|${row.origin}` },
|
|
613
|
+
regions: { columns: REGION_COLUMNS, map: toRegion, key: (row: RegionRow) => `${row.space}|${row.start}|${row.end}|${row.type}|${row.element ?? ""}|${row.origin}` },
|
|
614
|
+
comments: { columns: COMMENT_COLUMNS, map: toComment, key: (row: CommentRow) => `${row.space}|${row.address}|${row.placement}|${row.text}|${row.origin}` },
|
|
615
|
+
references: { columns: REFERENCE_COLUMNS, map: toReference, key: (row: ReferenceRow) => `${row.space}|${row.from}|${row.to}|${row.kind}|${row.origin}` },
|
|
616
|
+
modules: { columns: MODULE_COLUMNS, map: toModule, key: (row: ModuleRow) => `${row.space}|${row.start}|${row.file}|${row.title ?? ""}|${row.origin}` },
|
|
617
|
+
suppressions: {
|
|
618
|
+
columns: SUPPRESSION_COLUMNS,
|
|
619
|
+
map: toSuppression,
|
|
620
|
+
key: (row: SuppressionRow) => `${row.space}|${row.entity}|${row.start}|${row.end}|${row.to ?? ""}|${row.kind ?? ""}|${row.origin}`,
|
|
621
|
+
},
|
|
622
|
+
} as const;
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Makes the current knowledge equal to what it was at `revision`, as a new
|
|
626
|
+
* revision. History is never erased; restored rows keep their origin.
|
|
627
|
+
*/
|
|
628
|
+
export function revert(db: DatabaseSync, context: WriteContext, request: { revision: number }): WriteResult<{ restoredRevision: number }> {
|
|
629
|
+
return write(db, context, "revert", (change) => {
|
|
630
|
+
const current = currentRevision(db);
|
|
631
|
+
if (!Number.isInteger(request.revision) || request.revision < 0 || request.revision > current) {
|
|
632
|
+
throw new KnowledgeError("not-found", `There is no revision ${request.revision}; knowledge is at revision ${current}.`);
|
|
633
|
+
}
|
|
634
|
+
const at = "valid_from_revision <= ? AND (valid_to_revision IS NULL OR valid_to_revision > ?)";
|
|
635
|
+
for (const [table, spec] of Object.entries(KEYS)) {
|
|
636
|
+
const name = table === "references" ? '"references"' : table;
|
|
637
|
+
const then = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE ${at}`).all(request.revision, request.revision) as Array<Record<string, unknown>>;
|
|
638
|
+
const now = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE valid_to_revision IS NULL`).all() as Array<Record<string, unknown>>;
|
|
639
|
+
const key = (row: Record<string, unknown>) => (spec.key as (row: unknown) => string)(spec.map(row as never));
|
|
640
|
+
const thenKeys = new Set(then.map(key));
|
|
641
|
+
const nowKeys = new Set(now.map(key));
|
|
642
|
+
// Close first, so restored rows never collide with the current-row rules.
|
|
643
|
+
for (const row of now) if (!thenKeys.has(key(row))) change.close(name, "id = ?", row.id as number);
|
|
644
|
+
for (const row of then) {
|
|
645
|
+
if (nowKeys.has(key(row))) continue;
|
|
646
|
+
if (table === "suppressions") {
|
|
647
|
+
change.insertSuppression(spec.map(row as never) as SuppressionRow);
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (table === "modules") {
|
|
651
|
+
change.insertModule(spec.map(row as never) as ModuleRow);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const mapped = spec.map(row as never) as SymbolRow & RegionRow & CommentRow & ReferenceRow;
|
|
655
|
+
if (table === "symbols") change.insertSymbol(mapped);
|
|
656
|
+
else if (table === "regions") change.insertRegion(mapped);
|
|
657
|
+
else if (table === "comments") change.insertComment(mapped);
|
|
658
|
+
else change.insertReference(mapped);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return { restoredRevision: request.revision };
|
|
662
|
+
});
|
|
663
|
+
}
|