@henols/c64-re-tools 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +7 -3
- package/skills/c64-assembler/scripts/assembler.ts +29 -11
- package/skills/c64-assembler/scripts/lib/compare.ts +4 -3
- package/skills/c64-basic/SKILL.md +1 -0
- package/skills/c64-disk/SKILL.md +3 -2
- package/skills/c64-disk/scripts/disk.ts +26 -12
- package/skills/c64-disk/scripts/lib/containers.ts +10 -11
- package/skills/c64-docs/SKILL.md +2 -1
- package/skills/c64-docs/scripts/docs.ts +1 -1
- package/skills/c64-docs/scripts/lib/links.ts +5 -3
- package/skills/c64-docs/scripts/lib/reference.ts +2 -2
- package/skills/c64-emulator/SKILL.md +9 -2
- package/skills/c64-knowledge/SKILL.md +9 -5
- package/skills/c64-knowledge/scripts/knowledge.ts +91 -53
- package/skills/c64-knowledge/scripts/lib/listing.ts +8 -30
- package/skills/c64-memory-map/SKILL.md +2 -1
- package/skills/c64-memory-map/scripts/memory-map.ts +4 -3
- package/skills/c64-provenance/SKILL.md +6 -4
- package/skills/c64-provenance/scripts/lib/library.ts +18 -6
- package/skills/c64-provenance/scripts/lib/separate.ts +45 -6
- package/skills/c64-provenance/scripts/provenance.ts +14 -10
- package/skills/c64-source/SKILL.md +3 -0
- package/skills/c64-source/scripts/lib/source.ts +9 -31
- package/skills/c64-source/scripts/source.ts +14 -10
- package/skills/c64-static-analysis/SKILL.md +2 -0
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +6 -2
- package/skills/c64-static-analysis/scripts/static-analysis.ts +18 -8
- package/skills/c64-testing/SKILL.md +14 -3
- package/skills/c64-testing/references/recording-format.md +2 -0
- package/skills/c64-testing/references/scenario-format.md +6 -0
- package/skills/c64-testing/scripts/lib/diff-files.ts +76 -14
- package/skills/c64-testing/scripts/lib/diff.ts +3 -3
- package/skills/c64-testing/scripts/lib/recording.ts +50 -15
- package/skills/c64-testing/scripts/lib/replay.ts +12 -1
- package/skills/c64-testing/scripts/lib/runs.ts +84 -4
- package/skills/c64-testing/scripts/lib/scenario.ts +43 -6
- package/skills/c64-testing/scripts/testing.ts +20 -7
- package/skills/c64-unpacker/SKILL.md +17 -10
- package/skills/c64-unpacker/scripts/lib/evidence.ts +4 -5
- package/skills/c64-unpacker/scripts/lib/loads.ts +14 -7
- package/skills/c64-unpacker/scripts/lib/machine.ts +30 -3
- package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
- package/skills/c64-unpacker/scripts/unpacker.ts +93 -42
- package/src/host/vice/adapter.ts +4 -1
- package/src/host/vice/session.ts +148 -13
- package/src/host-client/image.ts +40 -8
- package/src/host-client/steps.ts +145 -9
- package/src/host-client/tools.ts +1 -1
- package/src/host-client/transfer.ts +33 -18
- package/src/knowledge/expression.ts +1 -1
- package/src/knowledge/history.ts +74 -4
- package/src/knowledge/import.ts +79 -7
- package/src/knowledge/names.ts +2 -1
- package/src/knowledge/program.ts +32 -18
- package/src/knowledge/read.ts +46 -0
- package/src/knowledge/schema.ts +4 -2
- package/src/knowledge/text.ts +2 -0
- package/src/knowledge/write.ts +58 -16
- package/src/mcp/inputs.ts +6 -3
- package/src/mcp/output.ts +5 -0
- package/src/mcp/server.ts +20 -4
- package/src/mcp/tools/debug.ts +36 -9
- package/src/mcp/tools/execution.ts +18 -5
- package/src/mcp/tools/machine.ts +3 -1
- package/src/mcp/tools/media.ts +14 -2
- package/src/mcp/tools/memory.ts +11 -6
- package/src/mcp/tools/video.ts +5 -3
- package/src/media.ts +42 -0
- package/src/opcodes.ts +51 -18
- package/src/project.ts +111 -3
- package/src/protocol/vice.ts +8 -2
- package/src/script.ts +55 -3
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// The host never sees a client path: only bytes and the file's type.
|
|
3
3
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
-
import {
|
|
5
|
+
import { lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, extname, join, relative, sep } from "node:path";
|
|
7
7
|
|
|
8
|
-
import { isInside, projectRoot, resolveProject, resolveProjectFile,
|
|
8
|
+
import { checkOutputPath, isInside, projectRoot, resolveProject, resolveProjectFile, writeProblem } from "../project.ts";
|
|
9
9
|
import { isRefusal } from "../errors.ts";
|
|
10
10
|
import { MAX_ATTACHMENT_BYTES } from "../protocol/framing.ts";
|
|
11
11
|
import { WireFailure } from "../protocol/messages.ts";
|
|
@@ -58,34 +58,49 @@ export function readProjectFile(path: string, options: { root?: string } = {}):
|
|
|
58
58
|
return { bytes: readFileSync(real), type: extname(path).slice(1).toLowerCase() };
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The refusal for a write that the file system refused because of the path
|
|
63
|
+
* (a folder, a file in place of a folder, no permission): invalid-input with
|
|
64
|
+
* the path. Undefined for another error, which is no fault of the path.
|
|
65
|
+
*/
|
|
66
|
+
export function writeRefusal(error: unknown, path: string, what?: string): WireFailure | undefined {
|
|
67
|
+
const message = writeProblem(error, path, what);
|
|
68
|
+
return message === undefined ? undefined : new WireFailure("invalid-input", message);
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
/**
|
|
62
72
|
* Writes bytes to a project file on the client side, making its directories
|
|
63
|
-
* and overwriting it.
|
|
64
|
-
*
|
|
73
|
+
* and overwriting it. First the check of checkOutputPath, which a script also
|
|
74
|
+
* does on its output options before any work. Throws WireFailure(invalid-input)
|
|
75
|
+
* for a path outside the project, also one that leaves it through a symbolic
|
|
76
|
+
* link, for a path in the knowledge, and for a path that the file system
|
|
77
|
+
* refuses: a folder, a file in place of a folder, no permission.
|
|
65
78
|
*/
|
|
66
79
|
export function writeProjectFile(path: string, bytes: Uint8Array, options: { root?: string; what?: string } = {}): void {
|
|
67
80
|
const root = options.root ?? currentProject();
|
|
68
81
|
const refuse = () => new WireFailure("invalid-input", `${options.what ?? path}: give a path relative to the project folder that stays inside it, not ${path}.`);
|
|
69
82
|
let file: string;
|
|
70
83
|
try {
|
|
71
|
-
file =
|
|
72
|
-
} catch {
|
|
73
|
-
throw
|
|
84
|
+
file = checkOutputPath(path, "file", { root, ...(options.what === undefined ? {} : { what: options.what }) });
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (isRefusal(error)) throw new WireFailure(error.code, error.message);
|
|
87
|
+
throw error;
|
|
74
88
|
}
|
|
75
|
-
// The nearest folder that exists must be inside the project before a folder is made below it.
|
|
76
|
-
let existing = dirname(file);
|
|
77
|
-
while (!existsSync(existing)) existing = dirname(existing);
|
|
78
|
-
if (!isInside(realpathSync(root), realpathSync(existing))) throw refuse();
|
|
79
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
80
|
-
if (!isInside(realpathSync(root), realpathSync(dirname(file)))) throw refuse();
|
|
81
|
-
let link = false;
|
|
82
89
|
try {
|
|
83
|
-
|
|
90
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
91
|
+
// Again after the folders are made: another process can change the folders between the check and the write.
|
|
92
|
+
if (!isInside(realpathSync(root), realpathSync(dirname(file)))) throw refuse();
|
|
93
|
+
let link = false;
|
|
94
|
+
try {
|
|
95
|
+
link = lstatSync(file).isSymbolicLink();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
98
|
+
}
|
|
99
|
+
if (link) throw refuse();
|
|
100
|
+
writeFileSync(file, bytes);
|
|
84
101
|
} catch (error) {
|
|
85
|
-
|
|
102
|
+
throw writeRefusal(error, path, options.what) ?? error;
|
|
86
103
|
}
|
|
87
|
-
if (link) throw refuse();
|
|
88
|
-
writeFileSync(file, bytes);
|
|
89
104
|
}
|
|
90
105
|
|
|
91
106
|
/** Directories never staged: version control and the toolkit's own project state. */
|
|
@@ -119,7 +119,7 @@ function value(node: Node, resolve: (name: string) => number | undefined): numbe
|
|
|
119
119
|
return node.value;
|
|
120
120
|
case "name": {
|
|
121
121
|
const found = resolve(node.name);
|
|
122
|
-
if (found === undefined) throw new Refusal("invalid-input",
|
|
122
|
+
if (found === undefined) throw new Refusal("invalid-input", `${node.name} is not a symbol of the knowledge or a platform name. Give a number, a symbol that knowledge.ts symbols lists, or a platform name that memory-map.ts at gives.`);
|
|
123
123
|
return found;
|
|
124
124
|
}
|
|
125
125
|
case "unary": {
|
package/src/knowledge/history.ts
CHANGED
|
@@ -37,8 +37,8 @@ import {
|
|
|
37
37
|
type SuppressionRow,
|
|
38
38
|
type SymbolRow,
|
|
39
39
|
} from "./read.ts";
|
|
40
|
-
import { DEFAULT_SPACE, type EvidenceKind, type Origin, type Space } from "./schema.ts";
|
|
41
|
-
import { compareText } from "./text.ts";
|
|
40
|
+
import { DEFAULT_SPACE, type CoverageKind, type EvidenceKind, type Origin, type Space } from "./schema.ts";
|
|
41
|
+
import { compareText, NO_REVISION } from "./text.ts";
|
|
42
42
|
import type { Evidence } from "./write.ts";
|
|
43
43
|
|
|
44
44
|
export interface Revision {
|
|
@@ -49,12 +49,49 @@ export interface Revision {
|
|
|
49
49
|
description?: string;
|
|
50
50
|
inputHash?: string;
|
|
51
51
|
toolVersion?: string;
|
|
52
|
+
/** A revert: the revision whose knowledge it restored. */
|
|
53
|
+
restores?: string;
|
|
54
|
+
/** A revert of one space: that space. A revert without it restored every space. */
|
|
55
|
+
restoresSpace?: Space;
|
|
52
56
|
/** What the change rests on. */
|
|
53
57
|
evidence?: Evidence[];
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
/** A memory row of the program: the bytes from an address. */
|
|
61
|
+
export interface MemoryRow {
|
|
62
|
+
space: Space;
|
|
63
|
+
address: number;
|
|
64
|
+
/** The count of bytes in the row. */
|
|
65
|
+
bytes: number;
|
|
66
|
+
origin: Origin;
|
|
67
|
+
revision: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The entry of the program, and whether it has a machine state and stages. */
|
|
71
|
+
export interface EntryRow {
|
|
72
|
+
space: Space;
|
|
73
|
+
address: number;
|
|
74
|
+
state: boolean;
|
|
75
|
+
stages: number;
|
|
76
|
+
origin: Origin;
|
|
77
|
+
revision: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A coverage range of one kind. */
|
|
81
|
+
export interface CoverageRow {
|
|
82
|
+
space: Space;
|
|
83
|
+
kind: CoverageKind;
|
|
84
|
+
start: number;
|
|
85
|
+
end: number;
|
|
86
|
+
origin: Origin;
|
|
87
|
+
revision: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
56
90
|
/** One knowledge row with its validity: from its first revision until the revision that closed it. */
|
|
57
91
|
export type HistoryEntry =
|
|
92
|
+
| { entity: "memory"; row: MemoryRow; from: string; to?: string }
|
|
93
|
+
| { entity: "entry"; row: EntryRow; from: string; to?: string }
|
|
94
|
+
| { entity: "coverage"; row: CoverageRow; from: string; to?: string }
|
|
58
95
|
| { entity: "symbol"; row: SymbolRow; from: string; to?: string }
|
|
59
96
|
| { entity: "analyzer-name"; row: SymbolRow; from: string; to?: string }
|
|
60
97
|
| { entity: "region"; row: RegionRow; from: string; to?: string }
|
|
@@ -79,6 +116,8 @@ function toRevision(row: Raw): Revision {
|
|
|
79
116
|
if (row.description !== null) revision.description = row.description as string;
|
|
80
117
|
if (row.input_hash !== null) revision.inputHash = row.input_hash as string;
|
|
81
118
|
if (row.tool_version !== null) revision.toolVersion = row.tool_version as string;
|
|
119
|
+
if (row.restored_revision !== null) revision.restores = row.restored_revision as string;
|
|
120
|
+
if (row.restored_space !== null && row.restored_space !== undefined) revision.restoresSpace = row.restored_space as Space;
|
|
82
121
|
return revision;
|
|
83
122
|
}
|
|
84
123
|
|
|
@@ -170,8 +209,22 @@ export function revisions(db: DatabaseSync | undefined, options: { limit?: numbe
|
|
|
170
209
|
return (rows as Raw[]).map((row) => withEvidence(db, toRevision(row)));
|
|
171
210
|
}
|
|
172
211
|
|
|
212
|
+
/** Revision 0: the knowledge before every revision. It is empty, and no write made it, so it has no time, origin or evidence. */
|
|
213
|
+
export interface InitialRevision {
|
|
214
|
+
id: typeof NO_REVISION;
|
|
215
|
+
operation: "none";
|
|
216
|
+
description: string;
|
|
217
|
+
createdAt?: undefined;
|
|
218
|
+
origin?: undefined;
|
|
219
|
+
inputHash?: undefined;
|
|
220
|
+
toolVersion?: undefined;
|
|
221
|
+
restores?: undefined;
|
|
222
|
+
restoresSpace?: undefined;
|
|
223
|
+
evidence?: undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
173
226
|
export interface RevisionChanges {
|
|
174
|
-
revision: Revision;
|
|
227
|
+
revision: Revision | InitialRevision;
|
|
175
228
|
/** Rows this revision made current. */
|
|
176
229
|
added: HistoryEntry[];
|
|
177
230
|
/** Rows this revision closed. */
|
|
@@ -181,6 +234,7 @@ export interface RevisionChanges {
|
|
|
181
234
|
/** One revision and exactly what it changed. */
|
|
182
235
|
export function revision(db: DatabaseSync | undefined, text: string): RevisionChanges {
|
|
183
236
|
const id = resolveRevision(db, text);
|
|
237
|
+
if (id === NO_REVISION) return { revision: { id: NO_REVISION, operation: "none", description: "The knowledge before every revision. It is empty." }, added: [], closed: [] };
|
|
184
238
|
const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
|
|
185
239
|
if (db === undefined || raw === undefined) throw new Refusal("not-found", `There is no revision ${id}.`);
|
|
186
240
|
const tables = [
|
|
@@ -196,9 +250,25 @@ export function revision(db: DatabaseSync | undefined, text: string): RevisionCh
|
|
|
196
250
|
["macro", "macros", MACRO_COLUMNS, toMacro],
|
|
197
251
|
["macro-use", "macro_uses", MACRO_USE_COLUMNS, toMacroUse],
|
|
198
252
|
] as const;
|
|
253
|
+
// The program: its memory, its entry and its coverage.
|
|
254
|
+
const program = [
|
|
255
|
+
["memory", "memory", "space, address, length(bytes) / 2 AS bytes, origin, valid_from_revision", (row: Raw): MemoryRow => ({ space: row.space as Space, address: row.address as number, bytes: row.bytes as number, origin: row.origin as Origin, revision: row.valid_from_revision as string })],
|
|
256
|
+
[
|
|
257
|
+
"entry",
|
|
258
|
+
"entries",
|
|
259
|
+
"space, address, state IS NOT NULL AS has_state, json_array_length(coalesce(stages, '[]')) AS stage_count, origin, valid_from_revision",
|
|
260
|
+
(row: Raw): EntryRow => ({ space: row.space as Space, address: row.address as number, state: row.has_state === 1, stages: row.stage_count as number, origin: row.origin as Origin, revision: row.valid_from_revision as string }),
|
|
261
|
+
],
|
|
262
|
+
[
|
|
263
|
+
"coverage",
|
|
264
|
+
"coverage",
|
|
265
|
+
"space, kind, start_address, end_address, origin, valid_from_revision",
|
|
266
|
+
(row: Raw): CoverageRow => ({ space: row.space as Space, kind: row.kind as CoverageKind, start: row.start_address as number, end: row.end_address as number, origin: row.origin as Origin, revision: row.valid_from_revision as string }),
|
|
267
|
+
],
|
|
268
|
+
] as const;
|
|
199
269
|
const added: HistoryEntry[] = [];
|
|
200
270
|
const closed: HistoryEntry[] = [];
|
|
201
|
-
for (const [entity, table, columns, map] of tables) {
|
|
271
|
+
for (const [entity, table, columns, map] of [...tables, ...program]) {
|
|
202
272
|
for (const row of db.prepare(`SELECT ${columns}, valid_to_revision FROM ${table} WHERE valid_from_revision = ?`).all(id) as Raw[]) {
|
|
203
273
|
added.push(entry(entity, map(row), row));
|
|
204
274
|
}
|
package/src/knowledge/import.ts
CHANGED
|
@@ -77,7 +77,18 @@ export type ImportConflict =
|
|
|
77
77
|
current: { address: number; name: string; kind: SymbolKind; origin: Origin };
|
|
78
78
|
finding: { name: string; kind: SymbolKind };
|
|
79
79
|
}
|
|
80
|
-
| { category: "region"; start: number; end: number; current: { type: RegionType; origin: Origin }; finding: { type: RegionType } }
|
|
80
|
+
| { category: "region"; start: number; end: number; current: { type: RegionType; origin: Origin }; finding: { type: RegionType } }
|
|
81
|
+
| {
|
|
82
|
+
/** A data finding over code that a run executed or that the user or the LLM named: the finding is not imported. */
|
|
83
|
+
category: "code-evidence";
|
|
84
|
+
start: number;
|
|
85
|
+
end: number;
|
|
86
|
+
finding: { type: RegionType };
|
|
87
|
+
/** The first executed instruction start in the range. */
|
|
88
|
+
executed?: number;
|
|
89
|
+
/** The first code name (label or routine) of the user or the LLM in the range. */
|
|
90
|
+
symbol?: { address: number; name: string; kind: SymbolKind; origin: Origin };
|
|
91
|
+
};
|
|
81
92
|
|
|
82
93
|
export interface ImportCounts {
|
|
83
94
|
added: number;
|
|
@@ -252,16 +263,53 @@ const YIELDS_TO_HEURISTIC = new Set<Origin>(["dxa", "ghidra"]);
|
|
|
252
263
|
/** Analyzers whose data gives way to a code finding over an executed instruction start. */
|
|
253
264
|
const YIELDS_TO_EXECUTED_CODE = new Set<Origin>(["dxa", "ghidra"]);
|
|
254
265
|
|
|
266
|
+
/** The first address in the range that is an executed instruction start, or undefined. */
|
|
267
|
+
function firstExecuted(findings: NormalizedFindings, start: number, end: number): number | undefined {
|
|
268
|
+
if (findings.executed === undefined) return undefined;
|
|
269
|
+
for (let address = start; address <= end; address++) if (findings.executed(address)) return address;
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
255
273
|
/** True when an address in the range is an executed instruction start. */
|
|
256
274
|
function holdsExecuted(findings: NormalizedFindings, start: number, end: number): boolean {
|
|
257
|
-
|
|
258
|
-
for (let address = start; address <= end; address++) if (findings.executed(address)) return true;
|
|
259
|
-
return false;
|
|
275
|
+
return firstExecuted(findings, start, end) !== undefined;
|
|
260
276
|
}
|
|
261
277
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
278
|
+
/**
|
|
279
|
+
* The data findings of DXA or Ghidra that code evidence contradicts: an
|
|
280
|
+
* executed instruction start, or a code name (label or routine) of the user or
|
|
281
|
+
* the LLM, in the range. Evidence wins over an analyzer guess: such a finding
|
|
282
|
+
* is not imported, and the analyzer's own code there stays.
|
|
283
|
+
*/
|
|
284
|
+
function contradictedData(db: DatabaseSync, findings: NormalizedFindings, space: Space): Array<ImportConflict & { category: "code-evidence" }> {
|
|
285
|
+
if (!YIELDS_TO_EXECUTED_CODE.has(findings.analyzer)) return [];
|
|
286
|
+
const names = db.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_symbols WHERE space = ? AND origin IN ('user', 'llm') AND kind IN ('label', 'routine') AND address BETWEEN ? AND ? ORDER BY address LIMIT 1`);
|
|
287
|
+
const found: Array<ImportConflict & { category: "code-evidence" }> = [];
|
|
288
|
+
for (const region of findings.regions) {
|
|
289
|
+
if (isCode(region.type) || region.type === "dual-use") continue;
|
|
290
|
+
const executed = firstExecuted(findings, region.start, region.end);
|
|
291
|
+
const raw = names.get(space, region.start, region.end) as Record<string, unknown> | undefined;
|
|
292
|
+
if (executed === undefined && raw === undefined) continue;
|
|
293
|
+
const symbol = raw === undefined ? undefined : toSymbol(raw);
|
|
294
|
+
found.push({
|
|
295
|
+
category: "code-evidence",
|
|
296
|
+
start: region.start,
|
|
297
|
+
end: region.end,
|
|
298
|
+
finding: { type: region.type },
|
|
299
|
+
...(executed === undefined ? {} : { executed }),
|
|
300
|
+
...(symbol === undefined ? {} : { symbol: { address: symbol.address, name: symbol.name, kind: symbol.kind, origin: symbol.origin } }),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return found;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function importRegions(db: DatabaseSync, change: Change, given: NormalizedFindings, coverage: AddressRange[], result: ImportResult): void {
|
|
307
|
+
const { analyzer } = given;
|
|
308
|
+
const space = given.space ?? DEFAULT_SPACE;
|
|
309
|
+
// Data findings over code evidence are conflicts, not facts.
|
|
310
|
+
const contradicted = contradictedData(db, given, space);
|
|
311
|
+
result.conflicts.push(...contradicted);
|
|
312
|
+
const findings = { ...given, regions: given.regions.filter((region) => !contradicted.some((item) => item.start === region.start && item.end === region.end)) };
|
|
265
313
|
if (analyzer === "heuristic") {
|
|
266
314
|
// An idiom is specific evidence about its bytes: the DXA or Ghidra region there gives way, its parts outside stay.
|
|
267
315
|
for (const region of findings.regions) {
|
|
@@ -333,6 +381,14 @@ function importRegions(db: DatabaseSync, change: Change, findings: NormalizedFin
|
|
|
333
381
|
if (suppressed.length > 0) result.regions.suppressed++;
|
|
334
382
|
for (const piece of subtract(region, [...protectedRows, ...suppressed])) desired.set(`${piece.start}-${piece.end}`, { ...piece, type: region.type });
|
|
335
383
|
}
|
|
384
|
+
// Under a contradicted data finding, the analyzer's own regions stay: they join the findings of the same type next to them.
|
|
385
|
+
for (const row of own) {
|
|
386
|
+
for (const item of contradicted) {
|
|
387
|
+
const inside = { start: Math.max(row.start, item.start, range.start), end: Math.min(row.end, item.end, range.end) };
|
|
388
|
+
if (inside.start > inside.end) continue;
|
|
389
|
+
for (const piece of subtract(inside, [...protectedRows, ...suppressionsOverlapping(db, "region", inside.start, inside.end, space)])) keepPiece(desired, { ...piece, type: row.type });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
336
392
|
const retiredRanges = new Set<string>();
|
|
337
393
|
for (const row of own) {
|
|
338
394
|
const inside = { start: Math.max(row.start, range.start), end: Math.min(row.end, range.end) };
|
|
@@ -357,6 +413,22 @@ function importRegions(db: DatabaseSync, change: Change, findings: NormalizedFin
|
|
|
357
413
|
}
|
|
358
414
|
}
|
|
359
415
|
|
|
416
|
+
/** Puts a kept piece into the desired regions, joined with the pieces of the same type that touch or overlap it. */
|
|
417
|
+
function keepPiece(desired: Map<string, { start: number; end: number; type: RegionType }>, piece: { start: number; end: number; type: RegionType }): void {
|
|
418
|
+
let joined = { ...piece };
|
|
419
|
+
for (let found = true; found; ) {
|
|
420
|
+
found = false;
|
|
421
|
+
for (const [key, other] of desired) {
|
|
422
|
+
if (other.type !== joined.type || other.end + 1 < joined.start || joined.end + 1 < other.start) continue;
|
|
423
|
+
desired.delete(key);
|
|
424
|
+
joined = { start: Math.min(joined.start, other.start), end: Math.max(joined.end, other.end), type: joined.type };
|
|
425
|
+
found = true;
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
desired.set(`${joined.start}-${joined.end}`, joined);
|
|
430
|
+
}
|
|
431
|
+
|
|
360
432
|
function importReferences(db: DatabaseSync, change: Change, findings: NormalizedFindings, coverage: AddressRange[], result: ImportResult): void {
|
|
361
433
|
const { analyzer } = findings;
|
|
362
434
|
const space = findings.space ?? DEFAULT_SPACE;
|
package/src/knowledge/names.ts
CHANGED
|
@@ -20,7 +20,8 @@ export function addressOfName(db: DatabaseSync | undefined, name: string, space:
|
|
|
20
20
|
if (addresses.length === 1) return addresses[0]!;
|
|
21
21
|
const where = space === DEFAULT_SPACE ? "" : ` in ${space}`;
|
|
22
22
|
if (addresses.length > 1) throw new Refusal("invalid-input", `the analyzers give ${name} to ${rows.map((row) => `${formatC64Address(row.address)} (${row.origin})`).join(" and ")}${where}. Give the address`);
|
|
23
|
-
|
|
23
|
+
const option = space === DEFAULT_SPACE ? "" : ` --space ${space}`;
|
|
24
|
+
throw new Refusal("not-found", `${name} is not a name in the knowledge${where}. Give a $ address, or a name that knowledge.ts symbols${option} lists, with or without --analyzer.`);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/** Every name of a space with its address: the symbols, and each analyzer name that addressOfName accepts. With kinds, only names of those kinds. */
|
package/src/knowledge/program.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { Refusal } from "../errors.ts";
|
|
|
10
10
|
import { type AddressRange, COVERAGE, formatC64Address } from "../c64.ts";
|
|
11
11
|
|
|
12
12
|
import { COVERAGE_KINDS, DEFAULT_SPACE, MEMORY_ROW, type CoverageKind, type Space } from "./schema.ts";
|
|
13
|
+
import { coverageRuns } from "./read.ts";
|
|
13
14
|
import { checkSpace, write, type Evidence, type WriteContext } from "./write.ts";
|
|
14
15
|
|
|
15
16
|
/** The 64 KiB of one space: offset n is address n. */
|
|
@@ -147,22 +148,6 @@ function currentCoverage(db: DatabaseSync, space: Space): Map<CoverageKind, Uint
|
|
|
147
148
|
return maps;
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
/** One import of a coverage map: its project path, its SHA-256, the space it went into and the revision. */
|
|
151
|
-
export interface CoverageRun {
|
|
152
|
-
name: string;
|
|
153
|
-
sha256: string;
|
|
154
|
-
space: Space;
|
|
155
|
-
revision: string;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** The imports of coverage maps, by name and then oldest first: the last import of a name is the latest. */
|
|
159
|
-
export function coverageRuns(db: DatabaseSync | undefined): CoverageRun[] {
|
|
160
|
-
if (db === undefined) return [];
|
|
161
|
-
return db
|
|
162
|
-
.prepare(`SELECT DISTINCT e.ref AS name, e.sha256 AS sha256, coalesce(e.space, '${DEFAULT_SPACE}') AS space, e.revision AS revision FROM evidence e JOIN revisions r ON r.id = e.revision WHERE r.operation = 'coverage' AND e.kind = 'coverage' ORDER BY e.ref, e.revision`)
|
|
163
|
-
.all() as unknown as CoverageRun[];
|
|
164
|
-
}
|
|
165
|
-
|
|
166
151
|
/**
|
|
167
152
|
* Joins coverage maps into the coverage of a space: adds the ranges of
|
|
168
153
|
* the flags that no imported run set before, and as wx the addresses that one
|
|
@@ -239,13 +224,42 @@ export function requireProgram(db: DatabaseSync | undefined, space: Space = DEFA
|
|
|
239
224
|
throw new Refusal("not-found", `The knowledge has no program${where}. Put it in with knowledge.ts image <image.json|prg>${option} of the c64-knowledge skill.`);
|
|
240
225
|
}
|
|
241
226
|
|
|
242
|
-
/** The
|
|
227
|
+
/** The pages of an entry state: $0000-$03ff as the capture wrote them, 256 bytes as hex for each page. */
|
|
228
|
+
const STATE_PAGES = ["$0000", "$0100", "$0200", "$0300"] as const;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The memory that the program has at its entry: the segments, and below them
|
|
232
|
+
* $0002-$03ff of the entry state (the zero page, the stack and the system
|
|
233
|
+
* pages hold code and data that the loader left). $0000 and $0001 are the
|
|
234
|
+
* CPU port, not memory. A segment byte wins over a state byte.
|
|
235
|
+
*/
|
|
236
|
+
export function entryMemory(image: ProgramImage): Uint8Array {
|
|
237
|
+
const memory = new Uint8Array(image.memory);
|
|
238
|
+
const pages = image.state?.pages;
|
|
239
|
+
if (typeof pages !== "object" || pages === null) return memory;
|
|
240
|
+
const inSegment = (address: number) => image.segments.some((segment) => address >= segment.start && address <= segment.end);
|
|
241
|
+
STATE_PAGES.forEach((page, index) => {
|
|
242
|
+
const hex = (pages as Record<string, unknown>)[page];
|
|
243
|
+
if (typeof hex !== "string" || !/^(?:[0-9a-fA-F]{2}){1,256}$/.test(hex)) return;
|
|
244
|
+
Buffer.from(hex, "hex").forEach((byte, offset) => {
|
|
245
|
+
const address = index * 0x100 + offset;
|
|
246
|
+
if (address >= 2 && !inSegment(address)) memory[address] = byte;
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
return memory;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The bytes for an analyzer: a PRG for one segment without an entry state,
|
|
254
|
+
* else the 64 KiB with the entry-state pages (entryMemory). DXA, Ghidra and
|
|
255
|
+
* c64-source get the same bytes.
|
|
256
|
+
*/
|
|
243
257
|
export function analyzedBytes(image: ProgramImage): { bytes: Buffer; kind: "prg" | "flat64k" } {
|
|
244
258
|
if (image.segments.length === 1 && image.state === undefined) {
|
|
245
259
|
const [segment] = image.segments;
|
|
246
260
|
return { bytes: Buffer.concat([Buffer.from([segment!.start & 0xff, segment!.start >> 8]), Buffer.from(image.memory.subarray(segment!.start, segment!.end + 1))]), kind: "prg" };
|
|
247
261
|
}
|
|
248
|
-
return { bytes: Buffer.from(image
|
|
262
|
+
return { bytes: Buffer.from(entryMemory(image)), kind: "flat64k" };
|
|
249
263
|
}
|
|
250
264
|
|
|
251
265
|
/**
|
package/src/knowledge/read.ts
CHANGED
|
@@ -243,6 +243,52 @@ export function currentRevision(db: DatabaseSync | undefined): string {
|
|
|
243
243
|
return (db.prepare("SELECT max(id) AS id FROM revisions").get() as { id: string | null }).id ?? "0";
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/** One import of a coverage map: its project path, its SHA-256, the space it went into and the revision. */
|
|
247
|
+
export interface CoverageRun {
|
|
248
|
+
name: string;
|
|
249
|
+
sha256: string;
|
|
250
|
+
space: Space;
|
|
251
|
+
revision: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The imports of coverage maps that the knowledge has, by name and then oldest
|
|
256
|
+
* first: the last import of a name is the latest. A revert gives back the maps
|
|
257
|
+
* of the revision that it restores, so a map that a revert removed is not known.
|
|
258
|
+
* A revert of one space gives back only the maps of that space.
|
|
259
|
+
* With `at`, the maps of the knowledge at that revision.
|
|
260
|
+
*/
|
|
261
|
+
export function coverageRuns(db: DatabaseSync | undefined, at?: string): CoverageRun[] {
|
|
262
|
+
if (db === undefined || at === NO_REVISION) return [];
|
|
263
|
+
const rows = db
|
|
264
|
+
.prepare(
|
|
265
|
+
`SELECT r.id AS id, r.operation AS operation, r.restored_revision AS restored, r.restored_space AS restoredSpace, e.ref AS name, e.sha256 AS sha256, coalesce(e.space, '${DEFAULT_SPACE}') AS space
|
|
266
|
+
FROM revisions r LEFT JOIN evidence e ON e.revision = r.id AND e.kind = 'coverage'
|
|
267
|
+
WHERE (r.operation = 'coverage' AND e.id IS NOT NULL) OR (r.operation = 'revert' AND r.restored_revision IS NOT NULL)
|
|
268
|
+
ORDER BY r.id, e.id`,
|
|
269
|
+
)
|
|
270
|
+
.all() as Array<{ id: string; operation: string; restored: string | null; restoredSpace: Space | null; name: string | null; sha256: string | null; space: Space }>;
|
|
271
|
+
// The maps after each revision that changed them, oldest first.
|
|
272
|
+
const after: Array<{ id: string; runs: CoverageRun[] }> = [];
|
|
273
|
+
let runs: CoverageRun[] = [];
|
|
274
|
+
for (const row of rows) {
|
|
275
|
+
if (at !== undefined && row.id > at) break;
|
|
276
|
+
if (row.operation === "revert") {
|
|
277
|
+
const restored = row.restored!;
|
|
278
|
+
let then: CoverageRun[] = [];
|
|
279
|
+
for (const item of after) if (item.id <= restored) then = item.runs;
|
|
280
|
+
const only = row.restoredSpace;
|
|
281
|
+
runs = only === null ? then : [...runs.filter((run) => run.space !== only), ...then.filter((run) => run.space === only)];
|
|
282
|
+
} else {
|
|
283
|
+
const run: CoverageRun = { name: row.name!, sha256: row.sha256!, space: row.space, revision: row.id };
|
|
284
|
+
if (!runs.some((item) => item.name === run.name && item.sha256 === run.sha256 && item.space === run.space && item.revision === run.revision)) runs = [...runs, run];
|
|
285
|
+
}
|
|
286
|
+
if (after.at(-1)?.id === row.id) after[after.length - 1]!.runs = runs;
|
|
287
|
+
else after.push({ id: row.id, runs });
|
|
288
|
+
}
|
|
289
|
+
return [...runs].sort((a, b) => (a.name === b.name ? (a.revision < b.revision ? -1 : a.revision > b.revision ? 1 : 0) : a.name < b.name ? -1 : 1));
|
|
290
|
+
}
|
|
291
|
+
|
|
246
292
|
/** At most this many ids are named when a start of an id fits several revisions. */
|
|
247
293
|
const NAMED_REVISIONS = 5;
|
|
248
294
|
|
package/src/knowledge/schema.ts
CHANGED
|
@@ -113,7 +113,9 @@ function createSchema(db: DatabaseSync): void {
|
|
|
113
113
|
operation TEXT NOT NULL,
|
|
114
114
|
input_hash TEXT,
|
|
115
115
|
tool_version TEXT,
|
|
116
|
-
description TEXT
|
|
116
|
+
description TEXT,
|
|
117
|
+
restored_revision TEXT,
|
|
118
|
+
restored_space TEXT
|
|
117
119
|
);
|
|
118
120
|
CREATE TABLE symbols (
|
|
119
121
|
id TEXT PRIMARY KEY,
|
|
@@ -316,7 +318,7 @@ function createSchema(db: DatabaseSync): void {
|
|
|
316
318
|
}
|
|
317
319
|
|
|
318
320
|
/** The version of the index this build makes. An index with another version is emptied and made again. */
|
|
319
|
-
export const INDEX_VERSION =
|
|
321
|
+
export const INDEX_VERSION = 8;
|
|
320
322
|
|
|
321
323
|
function indexVersion(db: DatabaseSync): number | undefined {
|
|
322
324
|
const hasMeta = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'meta'").get();
|
package/src/knowledge/text.ts
CHANGED
|
@@ -187,6 +187,8 @@ const REVISION_FIELDS: readonly Field[] = [
|
|
|
187
187
|
field("reason", "description", "text", true),
|
|
188
188
|
field("inputHash", "input_hash", "text", true),
|
|
189
189
|
field("toolVersion", "tool_version", "text", true),
|
|
190
|
+
field("restores", "restored_revision", "text", true),
|
|
191
|
+
field("restoresSpace", "restored_space", "text", true),
|
|
190
192
|
];
|
|
191
193
|
|
|
192
194
|
/** The SQL name of a table: "references" is a keyword. */
|