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