@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
|
@@ -44,10 +44,14 @@ Use only the MCP tools. Each MCP session has its own C64. Nothing else can chang
|
|
|
44
44
|
| Several of these from one moment | `c64_observe` |
|
|
45
45
|
| Keys, joystick | `c64_keyboard`, `c64_joystick` |
|
|
46
46
|
| Program and disk media | `c64_autostart`, `c64_program_load`, `c64_disk_attach` |
|
|
47
|
-
| Machine state to go back to | `c64_snapshot` |
|
|
47
|
+
| Machine state to go back to, also in a later session | `c64_snapshot` (with `out` and `file`) |
|
|
48
48
|
|
|
49
49
|
Each memory and register tool has a `space`. Use `c64` for the computer. Use `drive8` for the CPU of the 1541 disk drive. The memory `view` is `cpu` or `ram`. With `cpu`, you see what the CPU sees now, with ROM and I/O where the memory configuration puts them. With `ram`, you see the RAM under them.
|
|
50
50
|
|
|
51
|
+
For a large result, give `out` with a path in the project. `c64_memory_read`, `c64_disassemble`, `c64_cpu_history` and `c64_screen` then write the result to that file. The answer gives only the file, the number of bytes and a SHA-256. Use it for a memory dump of more than a few hundred bytes, a long listing, or a screen that the user wants to keep.
|
|
52
|
+
|
|
53
|
+
`c64_memmap` keeps RAM, ROM and I/O apart. Use action `summary` to see which code ran. Use action `read` with `out` to keep a coverage run as a file, for example `coverage/attract.bin`. Clear the map before each run that you want to keep apart.
|
|
54
|
+
|
|
51
55
|
## Workflow
|
|
52
56
|
|
|
53
57
|
1. Read the project knowledge for the addresses in the question. Use the c64-knowledge skill.
|
|
@@ -64,7 +68,11 @@ Write operations need a stopped CPU. Stop the CPU with `c64_execution` action `p
|
|
|
64
68
|
|
|
65
69
|
Step, next and until-return work only in space `c64`. The emulator cannot stop the disk drive CPU after one instruction. To find when the drive executes an address, use `c64_run_until` with an address target in space `drive8`, or a breakpoint in space `drive8`. The drive can execute a few more instructions before it stops. The computer completes its current instruction. The `pc` in the result is the drive address where it stopped.
|
|
66
70
|
|
|
67
|
-
`c64_keyboard` puts keys in the keyboard queue of the KERNAL. A program that reads the keyboard hardware directly does not see them. Many games do this.
|
|
71
|
+
`c64_keyboard` puts keys in the keyboard queue of the KERNAL. A program that reads the keyboard hardware directly does not see them. Many games do this. The emulator has no command that presses one key of the keyboard matrix. For such a program:
|
|
72
|
+
|
|
73
|
+
- A wait for any key that reads all keyboard columns at one time can end without a key. Joystick port 1 uses the same lines as the keyboard.
|
|
74
|
+
- For one specific key, for example F1 or RUN/STOP, open the window and ask the user to press the key.
|
|
75
|
+
- Do not change the program to make it accept a key.
|
|
68
76
|
|
|
69
77
|
Before a risky experiment, save the state with `c64_snapshot`. Restore it to try again from the same point.
|
|
70
78
|
|
|
@@ -108,9 +116,8 @@ If you cannot get the machine into the necessary state, say which state you coul
|
|
|
108
116
|
- `machine-unavailable` and the message says that the VICE window closed: the session starts the emulator again. Wait a few seconds. Then do the operation again.
|
|
109
117
|
- `machine-state-lost` with a different message: the emulator stopped and its state is gone. Do not continue with results from before. Tell the user to restart the MCP server.
|
|
110
118
|
- `machine-unavailable` and the message names a pause or a dialog in the emulator window: this occurs only while the window is open. Do not send more operations. Tell the user to resume the emulator (Pause, Alt+P) or to close the dialog in the VICE window. Then do the operation again. The machine state does not change.
|
|
111
|
-
- `machine-unavailable` with a different message: the host runtime does not run, or it does not answer. Tell the user to start the host runtime with `npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host`.
|
|
112
119
|
- `machine-running`: stop the CPU first, then try again.
|
|
113
|
-
- `installation-incomplete
|
|
120
|
+
- `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script. Do not try to repair the installation.
|
|
114
121
|
- `c64_run_until` with `stopReason` `timeout`: the target did not occur in the frame limit. This is evidence too. Report it.
|
|
115
122
|
- `c64_run_until` with `stopReason` `jam`: the CPU executed a JAM opcode and stopped. Report the address.
|
|
116
123
|
|
|
@@ -35,16 +35,41 @@ The script prints one JSON object. An error prints `{"error": {"code": ..., "mes
|
|
|
35
35
|
| Everything known at one address | `at '$2100'` |
|
|
36
36
|
| Find names and comment text | `search player` |
|
|
37
37
|
| Lists in a range | `symbols`, `regions`, `comments`, `references` with `--start` and `--end` |
|
|
38
|
+
| Only some origins | add `--origin semantic`, `--origin analyzer` (dxa, ghidra and heuristic), or `--origin user,llm` |
|
|
39
|
+
| All knowledge in one file | `export --out exports/knowledge.json` |
|
|
38
40
|
| Name the symbol at an address | `rename '$2100' update_player --kind routine` |
|
|
39
41
|
| Remove a symbol | `remove-symbol '$2100'` |
|
|
40
42
|
| Classify memory | `classify '$3000' '$303f' sprite` |
|
|
41
|
-
|
|
|
43
|
+
| Classify a table of records | `classify '$3000' '$303b' bytes --element 6` |
|
|
44
|
+
| Many writes as one revision | `apply batch.json`, or `apply -` with the JSON on standard input |
|
|
45
|
+
| Make memory unknown, and keep the analyzers out of it | `unclassify '$3000' '$303f'` |
|
|
42
46
|
| Set or remove a comment | `comment '$2100' line Reads joystick 2.` / `uncomment '$2100' line` |
|
|
43
47
|
| Add or remove a reference | `reference '$2000' '$2100' call` / `unreference ...` |
|
|
48
|
+
| Places that a removal blocks | `suppressions` with `--start` and `--end` |
|
|
49
|
+
| Start a file of generated source | `module '$2000' player.asm --title 'Player movement'` / `unmodule '$2000'` |
|
|
50
|
+
| Files of generated source | `modules` |
|
|
51
|
+
| The address spaces that hold knowledge | `spaces` |
|
|
44
52
|
| History | `history '$2100'`, `history --name update_player`, `revisions`, `revision 12` |
|
|
45
53
|
| Go back to an earlier state | `revert 12` |
|
|
46
54
|
|
|
47
|
-
|
|
55
|
+
Every command takes `--space`: `c64` (the default), `drive8` for code in the 1541 drive, or `bank0` to `bank255` for cartridge banks. Drive code and banks use the same addresses as the C64, so each space has its own knowledge. A name is unique inside one space. In a batch, a write can name its own `space`.
|
|
56
|
+
|
|
57
|
+
Symbol kinds are `label`, `routine`, `variable`, `data` and `vector`. Reference kinds are `call`, `jump`, `read`, `write` and `reference`.
|
|
58
|
+
|
|
59
|
+
Region types:
|
|
60
|
+
|
|
61
|
+
| Type | Meaning |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `code` | Instructions. |
|
|
64
|
+
| `bytes`, `words`, `petscii`, `screen`, `bitmap`, `sprite` | Data of that form. |
|
|
65
|
+
| `addresses`, `addresses-split` | A table of addresses: one table of 16-bit values, or a low-byte table and then a high-byte table. |
|
|
66
|
+
| `rts-addresses`, `rts-addresses-split` | The same, with each value the target minus 1, for a PHA, PHA, RTS dispatch. |
|
|
67
|
+
| `jumps` | A table of `JMP abs` instructions. Each 3-byte entry is an entry point. It is code. |
|
|
68
|
+
| `workspace` | RAM that the program writes at run time. Its bytes in the image do not matter. |
|
|
69
|
+
| `unused` | Bytes that nothing executes or reads. |
|
|
70
|
+
| `dual-use` | Code at one time and data or a vector at another time. No analyzer finding conflicts with it. |
|
|
71
|
+
|
|
72
|
+
`--element` gives the bytes of one element of a table. The length of the region must be a multiple of it. The result shows `element` and `count`.
|
|
48
73
|
|
|
49
74
|
## Workflow
|
|
50
75
|
|
|
@@ -57,11 +82,35 @@ Write:
|
|
|
57
82
|
|
|
58
83
|
1. Read the current value with `at`. Note the `revision` in the result.
|
|
59
84
|
2. Decide the change. Keep observed facts apart from your conclusions.
|
|
60
|
-
3. Write it with `--reason` and `--expect-revision` set to the revision you read.
|
|
85
|
+
3. Write it with `--reason` and `--expect-revision` set to the revision you read. Add `--evidence` for each thing that the change rests on: `coverage:<file>`, `test:<file>`, `file:<file>` or `note:<text>`. The script keeps the SHA-256 of each file.
|
|
61
86
|
4. Read the result. A `revision` of `null` means that nothing changed.
|
|
62
87
|
|
|
63
88
|
Each write is one revision. A classification inside a region splits that region, so regions never overlap.
|
|
64
89
|
|
|
90
|
+
For more than a few writes, put them in one JSON file and use `apply`. The file is a list of writes. Each write has `op` and the arguments of the command with the same name, for example:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
[
|
|
94
|
+
{ "op": "rename", "address": "$2100", "name": "update_player", "kind": "routine" },
|
|
95
|
+
{ "op": "classify", "start": "$3000", "end": "$303b", "type": "bytes", "element": 6 },
|
|
96
|
+
{ "op": "comment", "address": "$2100", "placement": "line", "text": "Reads joystick 2." }
|
|
97
|
+
]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The ops are `rename`, `remove-symbol`, `classify`, `unclassify`, `comment`, `uncomment`, `reference`, `unreference`, `module` and `unmodule`. All writes become one revision. If the knowledge refuses one write, the script writes nothing, and the error names the write.
|
|
101
|
+
|
|
102
|
+
## Modules
|
|
103
|
+
|
|
104
|
+
A module tells the c64-source skill where a file of the generated source starts. The file runs to the next module or to the end of its segment. Each address has one module, and each file name has one module. A file name has letters, digits, underscores or hyphens, and ends in `.asm`. The title becomes the first line of the file. Modules have a history, and `revert` restores them.
|
|
105
|
+
|
|
106
|
+
## Removals
|
|
107
|
+
|
|
108
|
+
`remove-symbol`, `unclassify` and `unreference` block the place for DXA and Ghidra. An analysis does not add a symbol at that address, a region in that range or that reference again. This applies to every removed fact, also to a fact from the LLM.
|
|
109
|
+
|
|
110
|
+
- `suppressions` lists the blocked places with the reason of each removal. `at` and `history` also show them.
|
|
111
|
+
- A `rename`, `classify` or `reference` at a blocked place ends the block there, because your own fact is at the place then.
|
|
112
|
+
- A `revert` to a revision before the removal also ends the block.
|
|
113
|
+
|
|
65
114
|
## Knowledge
|
|
66
115
|
|
|
67
116
|
Write after interpretation:
|
|
@@ -100,3 +149,4 @@ A revert makes a new revision. The history stays complete.
|
|
|
100
149
|
|
|
101
150
|
- Runtime evidence → c64-emulator
|
|
102
151
|
- Static evidence → c64-static-analysis
|
|
152
|
+
- Source from the knowledge → c64-source
|
|
@@ -1,44 +1,63 @@
|
|
|
1
1
|
// The c64-knowledge script: reads and writes .c64-re-tools/knowledge.db in the
|
|
2
2
|
// project directory (the working directory) and prints one compact JSON result.
|
|
3
3
|
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
4
6
|
import type { DatabaseSync } from "node:sqlite";
|
|
5
7
|
import { parseArgs } from "node:util";
|
|
6
8
|
|
|
7
9
|
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
10
|
+
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
8
11
|
import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
|
|
9
12
|
import { historyAt, historyNamed, revision, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
|
|
10
13
|
import {
|
|
11
14
|
at,
|
|
15
|
+
currentRevision,
|
|
12
16
|
listComments,
|
|
17
|
+
listModules,
|
|
18
|
+
listSpaces,
|
|
13
19
|
listReferences,
|
|
14
20
|
listRegions,
|
|
21
|
+
listSuppressions,
|
|
15
22
|
listSymbols,
|
|
16
23
|
search,
|
|
17
24
|
type CommentRow,
|
|
25
|
+
type ModuleRow,
|
|
18
26
|
type ReferenceRow,
|
|
19
27
|
type RegionRow,
|
|
28
|
+
type SuppressionRow,
|
|
20
29
|
type SymbolRow,
|
|
21
30
|
} from "#src/knowledge/read.ts";
|
|
22
31
|
import {
|
|
23
32
|
COMMENT_PLACEMENTS,
|
|
33
|
+
DEFAULT_SPACE,
|
|
34
|
+
EVIDENCE_KINDS,
|
|
35
|
+
ORIGINS,
|
|
24
36
|
REFERENCE_KINDS,
|
|
25
37
|
REGION_TYPES,
|
|
38
|
+
SPACE,
|
|
26
39
|
SYMBOL_KINDS,
|
|
27
40
|
type CommentPlacement,
|
|
41
|
+
type Origin,
|
|
28
42
|
type ReferenceKind,
|
|
29
43
|
type RegionType,
|
|
30
44
|
type SymbolKind,
|
|
31
45
|
} from "#src/knowledge/schema.ts";
|
|
32
46
|
import {
|
|
33
47
|
addReference,
|
|
48
|
+
applyBatch,
|
|
34
49
|
classifyRegion,
|
|
35
50
|
removeComment,
|
|
51
|
+
removeModule,
|
|
36
52
|
removeReference,
|
|
37
53
|
removeSymbol,
|
|
38
54
|
renameSymbol,
|
|
39
55
|
revert,
|
|
40
56
|
setComment,
|
|
57
|
+
setModule,
|
|
41
58
|
unclassifyRegion,
|
|
59
|
+
type BatchOperation,
|
|
60
|
+
type Evidence,
|
|
42
61
|
type WriteContext,
|
|
43
62
|
} from "#src/knowledge/write.ts";
|
|
44
63
|
import { runScript, UsageError } from "#src/script.ts";
|
|
@@ -48,27 +67,44 @@ const USAGE = `knowledge.ts <command> [arguments] [options]
|
|
|
48
67
|
Read (no database needed):
|
|
49
68
|
at <address> everything known at one address
|
|
50
69
|
search <text> symbol names and comment texts
|
|
51
|
-
symbols [--start A --end A --kind K]
|
|
52
|
-
regions [--start A --end A --type T]
|
|
53
|
-
comments [--start A --end A]
|
|
54
|
-
references [--start A --end A --kind K]
|
|
70
|
+
symbols [--start A --end A --kind K --origin O]
|
|
71
|
+
regions [--start A --end A --type T --origin O]
|
|
72
|
+
comments [--start A --end A --origin O]
|
|
73
|
+
references [--start A --end A --kind K --origin O]
|
|
74
|
+
suppressions [--start A --end A] places that removals keep free of analyzer facts
|
|
75
|
+
modules [--start A --end A --origin O] where the files of generated source start
|
|
76
|
+
spaces the address spaces that hold knowledge
|
|
55
77
|
history <address> | history --name <name>
|
|
56
78
|
revisions [--limit N --before N]
|
|
57
79
|
revision <id>
|
|
80
|
+
export --out <file> [--start A --end A --origin O] all current knowledge to a project file
|
|
81
|
+
|
|
82
|
+
Every command takes --space S: c64 (the default), drive8 for code in the 1541
|
|
83
|
+
drive, or bank0 to bank255 for cartridge banks. A write in a batch can name its
|
|
84
|
+
own space.
|
|
85
|
+
|
|
86
|
+
Read option --origin O: user, llm, dxa, ghidra, heuristic, semantic (user and
|
|
87
|
+
llm) or analyzer (dxa, ghidra and heuristic), or a comma list. search takes it too.
|
|
58
88
|
|
|
59
89
|
Write (one revision each; creates the database when needed):
|
|
60
90
|
rename <address> <name> [--kind K]
|
|
61
91
|
remove-symbol <address>
|
|
62
|
-
classify <start> <end> <type>
|
|
92
|
+
classify <start> <end> <type> [--element N]
|
|
63
93
|
unclassify <start> <end>
|
|
64
94
|
comment <address> <line|side> <text>
|
|
65
95
|
uncomment <address> <line|side>
|
|
66
96
|
reference <from> <to> <kind>
|
|
67
97
|
unreference <from> <to> <kind>
|
|
98
|
+
module <start> <file.asm> [--title T] a file of generated source starts here
|
|
99
|
+
unmodule <start>
|
|
68
100
|
revert <revision>
|
|
101
|
+
apply <file> many writes from a JSON file ('-' reads standard input),
|
|
102
|
+
all as one revision, or none when one is refused
|
|
69
103
|
|
|
70
104
|
Write options:
|
|
71
105
|
--reason <text> why, kept with the revision
|
|
106
|
+
--evidence <kind:ref> what the change rests on: coverage:<file>, test:<file>,
|
|
107
|
+
file:<file> or note:<text> (repeatable)
|
|
72
108
|
--expect-revision <N> refuse the write if knowledge changed after revision N
|
|
73
109
|
--origin user|llm who decided (default llm)
|
|
74
110
|
|
|
@@ -94,10 +130,30 @@ function integer(value: string | undefined, what: string): number {
|
|
|
94
130
|
}
|
|
95
131
|
|
|
96
132
|
// Output shapes: addresses as $xxxx, nothing internal.
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
133
|
+
/** The space of a row, shown only when it is not the C64 (D53). */
|
|
134
|
+
const spaceOf = (row: { space: string }) => (row.space === "c64" ? {} : { space: row.space });
|
|
135
|
+
const symbol = (row: SymbolRow) => ({ ...spaceOf(row), address: formatC64Address(row.address), name: row.name, kind: row.kind, origin: row.origin, revision: row.revision });
|
|
136
|
+
const region = (row: RegionRow) => ({
|
|
137
|
+
...spaceOf(row),
|
|
138
|
+
start: formatC64Address(row.start),
|
|
139
|
+
end: formatC64Address(row.end),
|
|
140
|
+
type: row.type,
|
|
141
|
+
...(row.element === undefined ? {} : { element: row.element, count: (row.end - row.start + 1) / row.element }),
|
|
142
|
+
origin: row.origin,
|
|
143
|
+
revision: row.revision,
|
|
144
|
+
});
|
|
145
|
+
const comment = (row: CommentRow) => ({ ...spaceOf(row), address: formatC64Address(row.address), placement: row.placement, text: row.text, origin: row.origin, revision: row.revision });
|
|
146
|
+
const module = (row: ModuleRow) => ({ ...spaceOf(row), start: formatC64Address(row.start), file: row.file, ...(row.title === undefined ? {} : { title: row.title }), origin: row.origin, revision: row.revision });
|
|
147
|
+
const reference = (row: ReferenceRow) => ({ ...spaceOf(row), from: formatC64Address(row.from), to: formatC64Address(row.to), kind: row.kind, origin: row.origin, revision: row.revision });
|
|
148
|
+
function suppression(row: SuppressionRow) {
|
|
149
|
+
const place =
|
|
150
|
+
row.entity === "symbol"
|
|
151
|
+
? { address: formatC64Address(row.start) }
|
|
152
|
+
: row.entity === "region"
|
|
153
|
+
? { start: formatC64Address(row.start), end: formatC64Address(row.end) }
|
|
154
|
+
: { from: formatC64Address(row.start), to: formatC64Address(row.to!), kind: row.kind! };
|
|
155
|
+
return { ...spaceOf(row), suppresses: row.entity, ...place, origin: row.origin, revision: row.revision };
|
|
156
|
+
}
|
|
101
157
|
|
|
102
158
|
function historyEntry(entry: HistoryEntry) {
|
|
103
159
|
const shown =
|
|
@@ -107,11 +163,27 @@ function historyEntry(entry: HistoryEntry) {
|
|
|
107
163
|
? region(entry.row)
|
|
108
164
|
: entry.entity === "comment"
|
|
109
165
|
? comment(entry.row)
|
|
110
|
-
:
|
|
166
|
+
: entry.entity === "reference"
|
|
167
|
+
? reference(entry.row)
|
|
168
|
+
: entry.entity === "module"
|
|
169
|
+
? module(entry.row)
|
|
170
|
+
: suppression(entry.row);
|
|
111
171
|
const { revision: _revision, ...row } = shown;
|
|
112
172
|
return { entity: entry.entity, ...row, fromRevision: entry.from, ...(entry.to === undefined ? {} : { toRevision: entry.to }) };
|
|
113
173
|
}
|
|
114
174
|
|
|
175
|
+
/** The reason of each revision that wrote one of the rows. */
|
|
176
|
+
function reasonsOf(db: DatabaseSync | undefined, rows: SuppressionRow[]): Map<number, string> {
|
|
177
|
+
const reasons = new Map<number, string>();
|
|
178
|
+
const query = db?.prepare("SELECT description FROM revisions WHERE id = ?");
|
|
179
|
+
for (const { revision } of rows) {
|
|
180
|
+
if (query === undefined || reasons.has(revision)) continue;
|
|
181
|
+
const found = query.get(revision) as { description: string | null } | undefined;
|
|
182
|
+
if (found?.description != null) reasons.set(revision, found.description);
|
|
183
|
+
}
|
|
184
|
+
return reasons;
|
|
185
|
+
}
|
|
186
|
+
|
|
115
187
|
function run(argv: string[]): unknown {
|
|
116
188
|
const { values, positionals } = parseArgs({
|
|
117
189
|
args: argv,
|
|
@@ -128,48 +200,88 @@ function run(argv: string[]): unknown {
|
|
|
128
200
|
reason: { type: "string" },
|
|
129
201
|
"expect-revision": { type: "string" },
|
|
130
202
|
origin: { type: "string" },
|
|
203
|
+
element: { type: "string" },
|
|
204
|
+
evidence: { type: "string", multiple: true },
|
|
205
|
+
out: { type: "string" },
|
|
206
|
+
title: { type: "string" },
|
|
207
|
+
space: { type: "string" },
|
|
131
208
|
help: { type: "boolean" },
|
|
132
209
|
},
|
|
133
210
|
});
|
|
134
211
|
const [command, ...args] = positionals;
|
|
135
212
|
if (values.help || command === undefined) throw new UsageError("");
|
|
136
213
|
const range = { start: values.start === undefined ? 0x0000 : address(values.start, "--start"), end: values.end === undefined ? 0xffff : address(values.end, "--end") };
|
|
214
|
+
if (values.space !== undefined && !SPACE.test(values.space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
|
|
215
|
+
const space = values.space ?? DEFAULT_SPACE;
|
|
137
216
|
|
|
138
|
-
const read = new Set(["at", "search", "symbols", "regions", "comments", "references", "history", "revisions", "revision"]);
|
|
139
|
-
const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "revert"]);
|
|
217
|
+
const read = new Set(["at", "search", "symbols", "regions", "comments", "references", "suppressions", "modules", "spaces", "history", "revisions", "revision", "export"]);
|
|
218
|
+
const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "module", "unmodule", "revert", "apply"]);
|
|
140
219
|
// Refuse a mistyped command before anything opens, so no database appears by accident.
|
|
141
220
|
if (!read.has(command) && !write.has(command)) throw new UsageError(`unknown command: ${command}`);
|
|
142
221
|
if (read.has(command)) {
|
|
222
|
+
const origins = values.origin === undefined ? undefined : originFilter(values.origin);
|
|
223
|
+
if (command === "export" && values.out === undefined) throw new UsageError("export needs --out <file>");
|
|
143
224
|
const db = openForRead();
|
|
144
225
|
try {
|
|
145
226
|
switch (command) {
|
|
146
227
|
case "at": {
|
|
147
|
-
const known = at(db, address(args[0]));
|
|
228
|
+
const known = at(db, address(args[0]), space);
|
|
148
229
|
return {
|
|
230
|
+
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
149
231
|
address: formatC64Address(known.address),
|
|
150
232
|
...(known.symbol === undefined ? {} : { symbol: symbol(known.symbol) }),
|
|
151
233
|
...(known.region === undefined ? {} : { region: region(known.region) }),
|
|
152
234
|
comments: known.comments.map(comment),
|
|
153
235
|
referencesFrom: known.referencesFrom.map(reference),
|
|
154
236
|
referencesTo: known.referencesTo.map(reference),
|
|
237
|
+
...(known.suppressions.length === 0 ? {} : { suppressions: known.suppressions.map(suppression) }),
|
|
155
238
|
revision: known.revision,
|
|
156
239
|
};
|
|
157
240
|
}
|
|
158
241
|
case "search": {
|
|
159
242
|
if (args[0] === undefined || args[0] === "") throw new UsageError("search needs text");
|
|
160
|
-
const found = search(db, args[0], values.limit === undefined ? 50 : integer(values.limit, "--limit"));
|
|
243
|
+
const found = search(db, args[0], values.limit === undefined ? 50 : integer(values.limit, "--limit"), origins, space);
|
|
161
244
|
return { symbols: found.symbols.map(symbol), comments: found.comments.map(comment) };
|
|
162
245
|
}
|
|
163
246
|
case "symbols":
|
|
164
|
-
return { symbols: listSymbols(db, range, values.kind === undefined ? undefined : oneOf(SYMBOL_KINDS, values.kind, "--kind")).map(symbol) };
|
|
247
|
+
return { symbols: listSymbols(db, range, values.kind === undefined ? undefined : oneOf(SYMBOL_KINDS, values.kind, "--kind"), origins, space).map(symbol) };
|
|
165
248
|
case "regions":
|
|
166
|
-
return { regions: listRegions(db, range, values.type === undefined ? undefined : oneOf(REGION_TYPES, values.type, "--type")).map(region) };
|
|
249
|
+
return { regions: listRegions(db, range, values.type === undefined ? undefined : oneOf(REGION_TYPES, values.type, "--type"), origins, space).map(region) };
|
|
167
250
|
case "comments":
|
|
168
|
-
return { comments: listComments(db, range).map(comment) };
|
|
251
|
+
return { comments: listComments(db, range, origins, space).map(comment) };
|
|
169
252
|
case "references":
|
|
170
|
-
return { references: listReferences(db, range, values.kind === undefined ? undefined : oneOf(REFERENCE_KINDS, values.kind, "--kind")).map(reference) };
|
|
253
|
+
return { references: listReferences(db, range, values.kind === undefined ? undefined : oneOf(REFERENCE_KINDS, values.kind, "--kind"), origins, space).map(reference) };
|
|
254
|
+
case "export": {
|
|
255
|
+
const exported = {
|
|
256
|
+
revision: currentRevision(db),
|
|
257
|
+
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
258
|
+
symbols: listSymbols(db, range, undefined, origins, space).map(symbol),
|
|
259
|
+
regions: listRegions(db, range, undefined, origins, space).map(region),
|
|
260
|
+
comments: listComments(db, range, origins, space).map(comment),
|
|
261
|
+
references: listReferences(db, range, undefined, origins, space).map(reference),
|
|
262
|
+
suppressions: listSuppressions(db, range, space).map(suppression),
|
|
263
|
+
modules: listModules(db, range, origins, space).map(module),
|
|
264
|
+
};
|
|
265
|
+
const bytes = Buffer.from(`${JSON.stringify(exported, null, 2)}\n`, "utf8");
|
|
266
|
+
writeProjectFile(values.out!, bytes, { what: "--out" });
|
|
267
|
+
return {
|
|
268
|
+
out: values.out,
|
|
269
|
+
bytes: bytes.length,
|
|
270
|
+
counts: { symbols: exported.symbols.length, regions: exported.regions.length, comments: exported.comments.length, references: exported.references.length, suppressions: exported.suppressions.length, modules: exported.modules.length },
|
|
271
|
+
revision: exported.revision,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
case "suppressions": {
|
|
275
|
+
const rows = listSuppressions(db, range, space);
|
|
276
|
+
const reasons = reasonsOf(db, rows);
|
|
277
|
+
return { suppressions: rows.map((row) => ({ ...suppression(row), ...(reasons.has(row.revision) ? { reason: reasons.get(row.revision) } : {}) })) };
|
|
278
|
+
}
|
|
279
|
+
case "modules":
|
|
280
|
+
return { modules: listModules(db, range, origins, space).map(module) };
|
|
281
|
+
case "spaces":
|
|
282
|
+
return { spaces: listSpaces(db) };
|
|
171
283
|
case "history":
|
|
172
|
-
return { history: (values.name === undefined ? historyAt(db, address(args[0])) : historyNamed(db, values.name)).map(historyEntry) };
|
|
284
|
+
return { history: (values.name === undefined ? historyAt(db, address(args[0]), space) : historyNamed(db, values.name, space)).map(historyEntry) };
|
|
173
285
|
case "revisions": {
|
|
174
286
|
const options: { limit?: number; before?: number } = {};
|
|
175
287
|
if (values.limit !== undefined) options.limit = integer(values.limit, "--limit");
|
|
@@ -189,18 +301,117 @@ function run(argv: string[]): unknown {
|
|
|
189
301
|
const context: WriteContext = { origin: values.origin === undefined ? "llm" : oneOf(["user", "llm"] as const, values.origin, "--origin") };
|
|
190
302
|
if (values.reason !== undefined) context.description = values.reason;
|
|
191
303
|
if (values["expect-revision"] !== undefined) context.expectedRevision = integer(values["expect-revision"], "--expect-revision");
|
|
304
|
+
if (values.evidence !== undefined) context.evidence = values.evidence.map(evidenceOf);
|
|
192
305
|
// Every argument is checked before the database opens: a refused write creates no database.
|
|
193
|
-
|
|
306
|
+
if (values.title !== undefined && command !== "module") throw new UsageError("--title works only with module");
|
|
307
|
+
const operation = command === "apply" ? applyOperation(args, context, space) : writeOperation(command, args, values.kind, values.element, context, values.title, space);
|
|
194
308
|
return withWrite(operation);
|
|
195
309
|
}
|
|
196
310
|
|
|
311
|
+
/** --origin for reads: one origin, semantic (user and llm), analyzer (dxa and ghidra), or a comma list. */
|
|
312
|
+
function originFilter(value: string): Origin[] {
|
|
313
|
+
const origins = new Set<Origin>();
|
|
314
|
+
for (const part of value.split(",")) {
|
|
315
|
+
if (part === "semantic") origins.add("user").add("llm");
|
|
316
|
+
else if (part === "analyzer") origins.add("dxa").add("ghidra").add("heuristic");
|
|
317
|
+
else origins.add(oneOf(ORIGINS, part, "--origin"));
|
|
318
|
+
}
|
|
319
|
+
return [...origins];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** --evidence kind:ref. A file kind names a project file and keeps its SHA-256, so a later change of the file shows. */
|
|
323
|
+
function evidenceOf(value: string): Evidence {
|
|
324
|
+
const colon = value.indexOf(":");
|
|
325
|
+
const kind = oneOf(EVIDENCE_KINDS, colon < 0 ? value : value.slice(0, colon), "--evidence kind");
|
|
326
|
+
const ref = colon < 0 ? "" : value.slice(colon + 1);
|
|
327
|
+
if (ref === "") throw new UsageError(`--evidence needs ${kind}:<${kind === "note" ? "text" : "file"}>`);
|
|
328
|
+
if (kind === "note") return { kind, ref };
|
|
329
|
+
return { kind, ref, sha256: createHash("sha256").update(readProjectFile(ref).bytes).digest("hex") };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** apply <file>: the batch as BatchOperation values, checked before the database opens. */
|
|
333
|
+
function applyOperation(args: string[], context: WriteContext, space: string): WriteOperation {
|
|
334
|
+
if (args.length !== 1) throw new UsageError("apply needs one file, or - for standard input");
|
|
335
|
+
const text = args[0] === "-" ? readFileSync(0, "utf8") : readProjectFile(args[0]!).bytes.toString("utf8");
|
|
336
|
+
let parsed: unknown;
|
|
337
|
+
try {
|
|
338
|
+
parsed = JSON.parse(text);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
throw new UsageError(`the batch is not JSON: ${(error as Error).message}`);
|
|
341
|
+
}
|
|
342
|
+
if (!Array.isArray(parsed)) throw new UsageError("the batch is a JSON list of writes, for example [{\"op\": \"rename\", \"address\": \"$2100\", \"name\": \"update_player\"}]");
|
|
343
|
+
const operations = parsed.map((item, index) => batchOperation(item, `write ${index + 1}`, space));
|
|
344
|
+
return (db) => {
|
|
345
|
+
const applied = applyBatch(db, context, operations);
|
|
346
|
+
// replaced: writes that replaced a row of another origin, as previous does for one write.
|
|
347
|
+
return { revision: applied.revision, writes: applied.steps.length, replaced: applied.steps.filter((step) => step.previous !== undefined).length };
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const BATCH_FIELDS: Record<BatchOperation["op"], string[]> = {
|
|
352
|
+
rename: ["address", "name", "kind"],
|
|
353
|
+
"remove-symbol": ["address"],
|
|
354
|
+
classify: ["start", "end", "type", "element"],
|
|
355
|
+
unclassify: ["start", "end"],
|
|
356
|
+
comment: ["address", "placement", "text"],
|
|
357
|
+
uncomment: ["address", "placement"],
|
|
358
|
+
reference: ["from", "to", "kind"],
|
|
359
|
+
unreference: ["from", "to", "kind"],
|
|
360
|
+
module: ["start", "file", "title"],
|
|
361
|
+
unmodule: ["start"],
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
/** One write of a batch. A write names its own space, or takes the one of --space. */
|
|
365
|
+
function batchOperation(item: unknown, at: string, space: string): BatchOperation {
|
|
366
|
+
const operation = batchFields(item, at);
|
|
367
|
+
const own = (item as Record<string, unknown>).space;
|
|
368
|
+
if (own !== undefined && typeof own !== "string") throw new UsageError(`${at}: space must be a string`);
|
|
369
|
+
return { ...operation, space: own ?? space };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** The fields of one write of a batch, with its addresses parsed. */
|
|
373
|
+
function batchFields(item: unknown, at: string): BatchOperation {
|
|
374
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) throw new UsageError(`${at}: a write is an object with op`);
|
|
375
|
+
const fields = item as Record<string, unknown>;
|
|
376
|
+
const op = oneOf(Object.keys(BATCH_FIELDS) as Array<BatchOperation["op"]>, String(fields.op), `${at}: op`);
|
|
377
|
+
const unknown = Object.keys(fields).filter((key) => key !== "op" && key !== "space" && !BATCH_FIELDS[op].includes(key));
|
|
378
|
+
if (unknown.length > 0) throw new UsageError(`${at}: ${op} takes ${BATCH_FIELDS[op].join(", ")}, not ${unknown.join(", ")}`);
|
|
379
|
+
const text = (key: string) => {
|
|
380
|
+
if (typeof fields[key] !== "string") throw new UsageError(`${at}: ${key} must be a string`);
|
|
381
|
+
return fields[key] as string;
|
|
382
|
+
};
|
|
383
|
+
const place = (key: string) => address(text(key), `${at}: ${key}`);
|
|
384
|
+
switch (op) {
|
|
385
|
+
case "rename":
|
|
386
|
+
return { op, address: place("address"), name: text("name"), ...(fields.kind === undefined ? {} : { kind: oneOf(SYMBOL_KINDS, text("kind"), `${at}: kind`) }) };
|
|
387
|
+
case "remove-symbol":
|
|
388
|
+
return { op, address: place("address") };
|
|
389
|
+
case "classify": {
|
|
390
|
+
if (fields.element !== undefined && typeof fields.element !== "number") throw new UsageError(`${at}: element must be a number`);
|
|
391
|
+
return { op, start: place("start"), end: place("end"), type: oneOf(REGION_TYPES, text("type"), `${at}: type`), ...(fields.element === undefined ? {} : { element: fields.element as number }) };
|
|
392
|
+
}
|
|
393
|
+
case "unclassify":
|
|
394
|
+
return { op, start: place("start"), end: place("end") };
|
|
395
|
+
case "comment":
|
|
396
|
+
return { op, address: place("address"), placement: oneOf(COMMENT_PLACEMENTS, text("placement"), `${at}: placement`), text: text("text") };
|
|
397
|
+
case "uncomment":
|
|
398
|
+
return { op, address: place("address"), placement: oneOf(COMMENT_PLACEMENTS, text("placement"), `${at}: placement`) };
|
|
399
|
+
case "module":
|
|
400
|
+
return { op, start: place("start"), file: text("file"), ...(fields.title === undefined ? {} : { title: text("title") }) };
|
|
401
|
+
case "unmodule":
|
|
402
|
+
return { op, start: place("start") };
|
|
403
|
+
default:
|
|
404
|
+
return { op, from: place("from"), to: place("to"), kind: oneOf(REFERENCE_KINDS, text("kind"), `${at}: kind`) };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
197
408
|
type WriteOperation = (db: DatabaseSync) => unknown;
|
|
198
409
|
|
|
199
|
-
function writeOperation(command: string, args: string[], kind: string | undefined, context: WriteContext): WriteOperation {
|
|
410
|
+
function writeOperation(command: string, args: string[], kind: string | undefined, element: string | undefined, context: WriteContext, title: string | undefined, space: string): WriteOperation {
|
|
200
411
|
switch (command) {
|
|
201
412
|
case "rename": {
|
|
202
413
|
if (args[1] === undefined) throw new UsageError("name is missing");
|
|
203
|
-
const request: { address: number; name: string; kind?: SymbolKind } = { address: address(args[0]), name: args[1] };
|
|
414
|
+
const request: { space: string; address: number; name: string; kind?: SymbolKind } = { space, address: address(args[0]), name: args[1] };
|
|
204
415
|
if (kind !== undefined) request.kind = oneOf(SYMBOL_KINDS, kind, "--kind");
|
|
205
416
|
return (db) => {
|
|
206
417
|
const result = renameSymbol(db, context, request);
|
|
@@ -208,43 +419,61 @@ function writeOperation(command: string, args: string[], kind: string | undefine
|
|
|
208
419
|
};
|
|
209
420
|
}
|
|
210
421
|
case "remove-symbol": {
|
|
211
|
-
const request = { address: address(args[0]) };
|
|
422
|
+
const request = { space, address: address(args[0]) };
|
|
212
423
|
return (db) => ({ revision: removeSymbol(db, context, request).revision });
|
|
213
424
|
}
|
|
214
425
|
case "classify": {
|
|
215
|
-
const request = {
|
|
426
|
+
const request = {
|
|
427
|
+
space,
|
|
428
|
+
start: address(args[0], "start"),
|
|
429
|
+
end: address(args[1], "end"),
|
|
430
|
+
type: oneOf(REGION_TYPES, args[2], "type") as RegionType,
|
|
431
|
+
...(element === undefined ? {} : { element: integer(element, "--element") }),
|
|
432
|
+
};
|
|
216
433
|
return (db) => {
|
|
217
434
|
const result = classifyRegion(db, context, request);
|
|
218
435
|
return { revision: result.revision, regions: result.current.map(region), ...(result.previous === undefined ? {} : { previous: result.previous.map(region) }) };
|
|
219
436
|
};
|
|
220
437
|
}
|
|
221
438
|
case "unclassify": {
|
|
222
|
-
const request = { start: address(args[0], "start"), end: address(args[1], "end") };
|
|
439
|
+
const request = { space, start: address(args[0], "start"), end: address(args[1], "end") };
|
|
223
440
|
return (db) => ({ revision: unclassifyRegion(db, context, request).revision });
|
|
224
441
|
}
|
|
225
442
|
case "comment": {
|
|
226
443
|
if (args.length < 3) throw new UsageError("text is missing");
|
|
227
|
-
const request = { address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") as CommentPlacement, text: args.slice(2).join(" ") };
|
|
444
|
+
const request = { space, address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") as CommentPlacement, text: args.slice(2).join(" ") };
|
|
228
445
|
return (db) => {
|
|
229
446
|
const result = setComment(db, context, request);
|
|
230
447
|
return { revision: result.revision, comment: comment(result.current) };
|
|
231
448
|
};
|
|
232
449
|
}
|
|
233
450
|
case "uncomment": {
|
|
234
|
-
const request = { address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") };
|
|
451
|
+
const request = { space, address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") };
|
|
235
452
|
return (db) => ({ revision: removeComment(db, context, request).revision });
|
|
236
453
|
}
|
|
237
454
|
case "reference": {
|
|
238
|
-
const request = { from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") as ReferenceKind };
|
|
455
|
+
const request = { space, from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") as ReferenceKind };
|
|
239
456
|
return (db) => {
|
|
240
457
|
const result = addReference(db, context, request);
|
|
241
458
|
return { revision: result.revision, reference: reference(result.current) };
|
|
242
459
|
};
|
|
243
460
|
}
|
|
244
461
|
case "unreference": {
|
|
245
|
-
const request = { from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") };
|
|
462
|
+
const request = { space, from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") };
|
|
246
463
|
return (db) => ({ revision: removeReference(db, context, request).revision });
|
|
247
464
|
}
|
|
465
|
+
case "module": {
|
|
466
|
+
if (args[1] === undefined) throw new UsageError("file is missing");
|
|
467
|
+
const request = { space, start: address(args[0], "start"), file: args[1], ...(title === undefined ? {} : { title }) };
|
|
468
|
+
return (db) => {
|
|
469
|
+
const result = setModule(db, context, request);
|
|
470
|
+
return { revision: result.revision, module: module(result.current) };
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
case "unmodule": {
|
|
474
|
+
const request = { space, start: address(args[0], "start") };
|
|
475
|
+
return (db) => ({ revision: removeModule(db, context, request).revision });
|
|
476
|
+
}
|
|
248
477
|
case "revert": {
|
|
249
478
|
const request = { revision: integer(args[0], "revision") };
|
|
250
479
|
return (db) => ({ revision: revert(db, context, request).revision });
|
|
@@ -24,6 +24,7 @@ The script looks up local reference data. It does not use the host runtime.
|
|
|
24
24
|
node <skill>/scripts/memmap.ts at '$d020' '$0314' '$ffd2'
|
|
25
25
|
node <skill>/scripts/memmap.ts decode '$d018' '$15' --bank '$97'
|
|
26
26
|
node <skill>/scripts/memmap.ts list vic
|
|
27
|
+
node <skill>/scripts/memmap.ts export --out src/hw.asm
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
| Command | Result |
|
|
@@ -31,6 +32,7 @@ node <skill>/scripts/memmap.ts list vic
|
|
|
31
32
|
| `at <address> ...` | For each address: `kind` (`platform` or `application`), the area, the name and the meaning. |
|
|
32
33
|
| `decode <register> <value>` | The meaning of the value. Use `--bank` with the value of `$DD00` to get the addresses for `$D018`. |
|
|
33
34
|
| `list <vic\|sid\|cia1\|cia2\|kernal\|system>` | All entries of that group. |
|
|
35
|
+
| `export --out <file>` | Writes all platform names to a project file as an ACME include: one `NAME = $xxxx` line for each name, with its meaning as a comment. |
|
|
34
36
|
|
|
35
37
|
Write an address or a value as `$` and hex digits, as a decimal number, or as `%` and binary digits. Put a `$` value in single quotes in a shell.
|
|
36
38
|
|
|
@@ -45,6 +47,7 @@ The script prints one JSON object.
|
|
|
45
47
|
3. For an I/O register, `mirrorOf` shows the register that a mirror address repeats. `visible` tells that an address in `$D000`-`$DFFF` is visible only when `$01` shows I/O at `$D000`. This area holds the chip registers, the color RAM and the cartridge I/O areas.
|
|
46
48
|
4. For a byte in a multi-byte entry, for example the high byte of a vector, `entryStart` gives the first address of the entry.
|
|
47
49
|
5. To understand a value in a register, run `decode`. For the memory configuration, decode the value of `$01`.
|
|
50
|
+
6. For source that you write by hand, run `export` and include the file with `!source`. The c64-source skill uses the same names without the file.
|
|
48
51
|
|
|
49
52
|
The decoders are for `$01`, `$D010`, `$D011`, `$D015` to `$D01F` (the sprite and interrupt bits), `$D016`, `$D018`, `$DC0D`, `$DD00` and `$DD0D`.
|
|
50
53
|
|
|
@@ -59,7 +62,7 @@ A conclusion about the application can be project knowledge. For example: "the p
|
|
|
59
62
|
|
|
60
63
|
## Result
|
|
61
64
|
|
|
62
|
-
The platform meaning of each address, or the decoded meaning of a register value.
|
|
65
|
+
The platform meaning of each address, or the decoded meaning of a register value. For `export`: the file, the count of names and the bytes.
|
|
63
66
|
|
|
64
67
|
The exit status is 0 for a result. The exit status is 2 for a wrong argument.
|
|
65
68
|
|