@henols/c64-re-tools 2.0.0-rc.16 → 2.0.0-rc.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +2 -1
  3. package/skills/c64-basic/SKILL.md +2 -1
  4. package/skills/c64-disk/SKILL.md +2 -1
  5. package/skills/c64-docs/SKILL.md +138 -0
  6. package/skills/c64-docs/references/page-types.md +113 -0
  7. package/skills/c64-docs/references/style.md +43 -0
  8. package/skills/c64-docs/scripts/generate.ts +110 -0
  9. package/skills/c64-docs/scripts/lib/links.ts +155 -0
  10. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  11. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  12. package/skills/c64-docs/scripts/lib/source-map.ts +83 -0
  13. package/skills/c64-docs/scripts/link.ts +193 -0
  14. package/skills/c64-emulator/SKILL.md +3 -0
  15. package/skills/c64-knowledge/SKILL.md +31 -3
  16. package/skills/c64-knowledge/scripts/knowledge.ts +172 -12
  17. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  18. package/skills/c64-memory-map/SKILL.md +2 -1
  19. package/skills/c64-provenance/SKILL.md +2 -1
  20. package/skills/c64-reverse-engineering/SKILL.md +10 -3
  21. package/skills/c64-source/SKILL.md +10 -4
  22. package/skills/c64-source/scripts/generate.ts +139 -23
  23. package/skills/c64-source/scripts/lib/loader.ts +30 -2
  24. package/skills/c64-source/scripts/lib/source.ts +121 -26
  25. package/skills/c64-static-analysis/SKILL.md +13 -7
  26. package/skills/c64-static-analysis/scripts/analyze.ts +33 -5
  27. package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -21
  28. package/skills/c64-static-analysis/scripts/lib/findings.ts +5 -1
  29. package/skills/c64-static-analysis/scripts/lib/macros.ts +2 -2
  30. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  31. package/skills/c64-testing/SKILL.md +21 -1
  32. package/skills/c64-testing/references/scenario-format.md +6 -2
  33. package/skills/c64-testing/scripts/diff.ts +167 -0
  34. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  35. package/skills/c64-testing/scripts/lib/scenario.ts +14 -7
  36. package/skills/c64-testing/scripts/run.ts +64 -12
  37. package/skills/c64-unpacker/SKILL.md +4 -2
  38. package/skills/c64-unpacker/scripts/lib/stages.ts +12 -0
  39. package/skills/c64-unpacker/scripts/unpack.ts +23 -5
  40. package/src/c64.ts +88 -0
  41. package/src/graphics.ts +158 -0
  42. package/src/host/vice/adapter.ts +2 -1
  43. package/src/host/vice/screen.ts +1 -42
  44. package/src/host/vice/session.ts +120 -3
  45. package/src/host-client/tools.ts +1 -0
  46. package/src/host-client/vice-session.ts +16 -1
  47. package/src/knowledge/conflicts.ts +4 -0
  48. package/src/knowledge/graph.ts +104 -0
  49. package/src/knowledge/history.ts +3 -0
  50. package/src/knowledge/import.ts +51 -44
  51. package/src/knowledge/join.ts +211 -0
  52. package/src/knowledge/read.ts +36 -4
  53. package/src/knowledge/schema.ts +15 -1
  54. package/src/knowledge/text.ts +2 -0
  55. package/src/knowledge/write.ts +8 -1
  56. package/src/mcp/server.ts +1 -0
  57. package/src/mcp/tools/debug.ts +66 -2
  58. package/src/mcp/tools/execution.ts +22 -3
  59. package/src/native/ghidra/analyze.ts +2 -2
  60. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  61. package/src/native/types.ts +13 -7
  62. package/{skills/c64-source/scripts/lib → src}/opcodes.ts +58 -1
  63. package/src/platform.ts +40 -0
  64. package/src/protocol/vice.ts +83 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henols/c64-re-tools",
3
- "version": "2.0.0-rc.16",
3
+ "version": "2.0.0-rc.18",
4
4
  "description": "Agent-friendly Commodore 64 engineering toolkit for reverse engineering, debugging, development and testing.",
5
5
  "license": "MIT",
6
6
  "author": "Henrik Olsson",
@@ -43,7 +43,8 @@ The script prints one JSON object. The target CPU is the 6510. The output is a C
43
43
 
44
44
  - Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
45
45
  - If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
46
- - Keep the results, batches and step files that you save for yourself in `.c64-re-tools/local/` of the project. Git ignores that folder.
46
+ - Keep the results, batches and step files that you use only one time in `.c64-re-tools/local/` of the project. Git ignores that folder.
47
+ - Put a run file whose results are the evidence for a finding in `runs/`, with its results. Then git keeps the run file and the results, and another person can do the run again.
47
48
  - A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
48
49
 
49
50
  ## Workflow
@@ -29,7 +29,8 @@ The script prints one JSON object. Addresses are `$` and four hex digits.
29
29
 
30
30
  - Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
31
31
  - If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
32
- - Keep the results, batches and step files that you save for yourself in `.c64-re-tools/local/` of the project. Git ignores that folder.
32
+ - Keep the results, batches and step files that you use only one time in `.c64-re-tools/local/` of the project. Git ignores that folder.
33
+ - Put a run file whose results are the evidence for a finding in `runs/`, with its results. Then git keeps the run file and the results, and another person can do the run again.
33
34
  - A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
34
35
 
35
36
  ## Workflow
@@ -46,7 +46,8 @@ File names show the characters that the C64 shows in a directory list. A byte th
46
46
 
47
47
  - Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
48
48
  - If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
49
- - Keep the results, batches and step files that you save for yourself in `.c64-re-tools/local/` of the project. Git ignores that folder.
49
+ - Keep the results, batches and step files that you use only one time in `.c64-re-tools/local/` of the project. Git ignores that folder.
50
+ - Put a run file whose results are the evidence for a finding in `runs/`, with its results. Then git keeps the run file and the results, and another person can do the run again.
50
51
  - A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
51
52
 
52
53
  ## Workflow
@@ -0,0 +1,138 @@
1
+ ---
2
+ name: c64-docs
3
+ description: Use this skill to write the documentation of a C64 program. The documentation explains the flows, state machines, tables, data formats and graphics, with links to the source lines and to the assets. A script generates the reference pages from the knowledge and the source. You write the other pages by the method of this skill. A second script adds the links to each name and address and checks every link. Use it after the source is identical, or to keep the documentation correct after a change to the knowledge.
4
+ ---
5
+
6
+ # c64-docs
7
+
8
+ ## Purpose
9
+
10
+ Write documentation that tells how a program works, and that points to the code. The documentation is Markdown in the project, for example in `docs/`. It has two parts:
11
+
12
+ - **The reference.** `generate.ts` generates it from the knowledge, the source map of c64-source and the image. It has a page for each module. The page gives the routines, their callers and callees, the data and a call graph. It also has pages for the names, the variables, the tables, the memory map and the graphics. Do not edit these pages. The next run writes over them.
13
+ - **The pages that you write.** They tell how the program works. They give the flows, the state machines, the data formats and the task of each part. Use the page types in `references/page-types.md` and the rules in `references/style.md`.
14
+
15
+ `link.ts` connects the two parts. In each page that you write, a name in backticks gets a link to its line in the source. The address after the name gets a link to the entry of the name in the reference. When the source changes, `link.ts` moves the links to the new lines.
16
+
17
+ This skill does not change the knowledge.
18
+
19
+ ## Inputs
20
+
21
+ - The source directory of c64-source, with its `source-map.json`, for example `src`. The source must be from the current knowledge revision.
22
+ - The image of the source, for example `unpacked/image.json`. It decodes the tables and shows the graphics.
23
+ - The directory of the documentation, for example `docs`. The reference goes into a directory of its own, for example `docs/reference`.
24
+
25
+ ## Tools / execution path
26
+
27
+ The scripts run on this machine. They do not use the emulator or the host runtime.
28
+
29
+ ```
30
+ node <skill>/scripts/generate.ts --source src --out docs/reference --image unpacked/image.json
31
+ node <skill>/scripts/link.ts docs --source src
32
+ node <skill>/scripts/link.ts docs --source src --check
33
+ ```
34
+
35
+ | Option | Meaning |
36
+ |---|---|
37
+ | `--source src` | The source directory of c64-source. Both scripts read `src/source-map.json`. |
38
+ | `--out docs/reference` | The directory of the reference. `generate.ts` writes only there. It refuses to start when the directory holds a page that it did not write. |
39
+ | `--image unpacked/image.json` | The image. Without it, the tables are not decoded and the graphics page has no images. |
40
+ | `docs` | The directory of the documentation. `link.ts` reads every `.md` file below it. It changes only the pages that you wrote. It checks the pages of the reference. |
41
+ | `--reference <dir>` | The directory of the reference for `link.ts`. The default is `docs/reference` when it exists. |
42
+ | `--check` | `link.ts` writes nothing. It fails when a link is not up to date or a page has a problem. |
43
+
44
+ ### Project folder
45
+
46
+ - Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
47
+ - Give `--project <folder>` to each script when the working directory is not in the project.
48
+
49
+ ## Workflow
50
+
51
+ 1. Read the knowledge with the c64-knowledge skill. The documentation can only be as good as the knowledge. Each routine needs a name and a comment. Each table needs a region and a name. Each file of the source needs a module with a title. Record what is missing with c64-knowledge.
52
+ 2. Generate the source with c64-source. Make sure that `check.identical` is `true`. The script also writes `source-map.json`.
53
+ 3. Run `generate.ts`. Read `index.md` of the reference. A module with no routines, or a routine with no comment, shows what the knowledge does not have yet.
54
+ 4. Make the plan of the pages. Start from the modules and the call graphs. Find the main loop, the interrupt chain, the dispatch tables, the state machines and the data formats. Each subsystem gets one page. The entry page lists the pages. Use the page types in `references/page-types.md`.
55
+ 5. Write each page. Read the code and the knowledge for each fact. Run the program with c64-testing or c64-emulator when a fact needs a run. Write each name with its address, for example `` `draw_room` `$09BC` ``. Mark each fact with its evidence. Several subagents can write the pages at the same time, one page each. Give each subagent `references/page-types.md` and `references/style.md`.
56
+ 6. Record each new fact in the knowledge first: a name, a comment, a region or a module. Then the source and the reference show it too. A fact only in a page is lost at the next change of the knowledge.
57
+ 7. Run `link.ts`. Read `problems` and correct each one. Then run `link.ts --check`. Make sure that `problems.count` is 0 and that `notUpToDate.links` is 0.
58
+ 8. After each change to the knowledge, do steps 2, 3 and 7 again. The links then go to the new lines.
59
+
60
+ ## The links
61
+
62
+ `link.ts` changes these forms in the pages that you write. It does not change a code block, a mermaid diagram or an HTML comment.
63
+
64
+ | You write | `link.ts` writes |
65
+ |---|---|
66
+ | `` `draw_room` `$09BC` `` | `` [`draw_room`](../src/room_draw.asm#L6) [`$09BC`](reference/modules/room_draw.md#draw_room) `` |
67
+ | `` `draw_room` (`$09BC`) `` | the same links, in the brackets |
68
+ | `` `draw_room` `$0AAF` `` | `$0AAF` is a place inside `draw_room`. The address gets a link to its line in the source. |
69
+ | `` `room_script_vars+3` `$4E9A` `` | a name with an offset. The address must be the address of the name plus the offset. |
70
+ | `` `draw_room` `` | the name gets a link to its line. |
71
+
72
+ - A name is a name of the knowledge or of the source. A word in backticks that is no name stays as it is.
73
+ - In a list such as `` `a`/`b` `$1000`/`$1010` ``, each name gets its link. The addresses stay as they are.
74
+ - `link.ts` writes a link again when its line moves. A link that you made to another target stays as you wrote it.
75
+ - `link.ts` checks every link and image: the file must exist, a `#anchor` must exist in the page, and a `#L` line must exist in the file.
76
+
77
+ ## The reference
78
+
79
+ - `index.md`: each file of the source with its range, title, the number of routines and the number of names.
80
+ - `modules/<file>.md`: a call graph of the file, its data regions with their readers and writers, and an entry for each name. An entry has the comment, the callers, the table that holds it, and for a routine its calls, jumps, reads and writes.
81
+ - `variables.md`: each name outside the image, with the routines that read it and write it.
82
+ - `tables.md`: each table of addresses, jumps and words, entry by entry, with its target. An RTS table holds each target minus 1. The table gives the target.
83
+ - `memory-map.md`: each region in address order.
84
+ - `symbols.md`: each name in alphabetical order.
85
+ - `graphics.md`: each `sprite`, `bitmap`, `charset` and `screen` region. A sprite, a bitmap or a charset shows in hires and in multicolour, because the knowledge has no graphics mode. A charset shows 16 characters in each row. A screen shows as text.
86
+
87
+ ### Images and assets
88
+
89
+ - The reference draws each sprite, bitmap and charset region, and it decodes each table. To get the image of a graphic or the entries of a table, classify its region with c64-knowledge. Do not write a script that draws or decodes them.
90
+ - A script of the project decodes only a format of the program, for example a packed map or a music stream. Put the script in `tools/` of the project. Its output files are the assets, for example in `assets/rooms/`.
91
+ - When the code of the program makes the data, run that code in VICE with a run of c64-testing. Observe the memory, or capture the screen. Do not write a 6502 emulator in the script.
92
+
93
+ In the call graph, a solid arrow is a call, a thick arrow is a table entry, and a dotted arrow is a jump. A table entry goes from each routine that reads the table to each target in code. A table of data addresses gives no arrow.
94
+
95
+ ## Knowledge
96
+
97
+ Read:
98
+ - symbols, regions, comments, references and modules of the space of the source.
99
+
100
+ Automatic write:
101
+ - none.
102
+
103
+ Do not store:
104
+ - the pages, or the facts that are only in the pages. Record each fact with c64-knowledge.
105
+
106
+ ## Result
107
+
108
+ `generate.ts` gives one JSON object:
109
+
110
+ - `out`, `index` and `revision`.
111
+ - `pages`, `entries`, `tables`, `images` and `edges`: the counts of the reference.
112
+ - `image`: the image, or a note when `--image` is not given.
113
+
114
+ `link.ts` gives one JSON object:
115
+
116
+ - `pages`: the count of the pages that it read.
117
+ - `linked`: the count of the links that it wrote, and the pages that it changed. With `--check`, `notUpToDate` gives the links that are not up to date.
118
+ - `problems`: `count`, `byKind` and the first 50 problems. Each problem has `at` (the page and the line), `kind` and `message`.
119
+
120
+ The exit status is 0 when there is no problem. The exit status is 1 when there is a problem, or with `--check` when a link is not up to date. The exit status is 2 for a wrong argument.
121
+
122
+ ## Failure and conflicts
123
+
124
+ - `not-found` for `source-map.json`: generate the source with c64-source. Then run the script again.
125
+ - `invalid-input` that names an old revision: the knowledge changed after the source. Generate the source again, then run `generate.ts`.
126
+ - `invalid-input` that names a page in the reference directory: the page is not a page of `generate.ts`. Move the page out of the directory. Do not give `--out` a directory with pages that you wrote.
127
+ - `unknown-name`: the page uses a name that is not in the knowledge. Record the name with c64-knowledge, or correct the page.
128
+ - `wrong-address`: the address after the name is not the address of the name, and it is not a place inside it. The message gives the routine that holds the address. Correct the name or the address. For a value, write `` `scroll_moves` = `$10` ``.
129
+ - `missing-file`, `missing-anchor` or `missing-line`: a link goes to nothing. Correct the link. An image of an asset must exist in the project.
130
+ - `stale-reference`: the source or the reference is older than the knowledge. Do steps 2 and 3 of the workflow again.
131
+ - `conflict`: the knowledge has conflicts from a git merge. Run `knowledge.ts merge` of c64-knowledge.
132
+
133
+ ## Handoffs
134
+
135
+ - Names, comments, regions and modules → c64-knowledge
136
+ - The source and its source map → c64-source
137
+ - A fact that needs a run → c64-testing or c64-emulator
138
+ - What an address of the C64 is → c64-memory-map
@@ -0,0 +1,113 @@
1
+ # Page types
2
+
3
+ Each page that you write is one of these types. The types come from the documentation of the Bruce Lee field run. Use the parts of a type that the program needs. Do not add a part with no content.
4
+
5
+ ## 1. The entry page
6
+
7
+ One entry page for the program, for example `docs/README.md` or `docs/rooms.md` for one large subsystem. It has these parts, in this order:
8
+
9
+ 1. **One paragraph.** What the program is, and which release the documentation is for.
10
+ 2. **The table of pages.** One row for each page, with the link and one sentence about its content.
11
+ 3. **The layers.** A mermaid diagram of the layers of the program. The layers are the data, the code that runs one time, the main loop and the interrupts. Under the diagram, a numbered list tells how the layers work together.
12
+ 4. **The flow of a user.** A mermaid diagram of the steps of the player or user from start to end. An example is the order of the levels.
13
+ 5. **Findings.** A table of the facts that a reader does not expect, each with its evidence. Put a bug of the original program here.
14
+ 6. **The reference.** A link to `reference/index.md`.
15
+ 7. **Still open.** What is not known yet, and which run can find it.
16
+
17
+ ```markdown
18
+ # <Program>: <subject>
19
+
20
+ <What the program is. The release. What this documentation covers.>
21
+
22
+ | Page | Content |
23
+ |---|---|
24
+ | [parts/main_loop.md](parts/main_loop.md) | **The main loop.** <one sentence> |
25
+
26
+ ## 1. The layers
27
+
28
+ ~~~mermaid
29
+ flowchart TB
30
+ subgraph DATA["Data"]
31
+ ...
32
+ ~~~
33
+
34
+ ## 2. Findings
35
+
36
+ | Finding | Evidence |
37
+ |---|---|
38
+ | <fact> | Observed: <what run showed it> |
39
+
40
+ ## 3. Still open
41
+
42
+ - <question> — <the run that can answer it>
43
+ ```
44
+
45
+ ## 2. The subsystem page
46
+
47
+ One page for each subsystem, for example the main loop, the interrupt chain, the room generation or the sound player. It has these parts:
48
+
49
+ 1. **The scope.** A list of what the page covers, and the sources (the source files, the assets).
50
+ 2. **The conventions.** The terms of the page, for example "a pass is one run of `main_loop` `$1BC2`". Also the evidence labels. Use the conventions block of section 5.
51
+ 3. **The overview.** A short text diagram or a mermaid flowchart of the calls, from the entry of the subsystem to its end.
52
+ 4. **The catalogue.** A table of the data of the subsystem. Each row has the name, the address, the layout (bytes, entries, the meaning of each byte), the readers and the writers.
53
+ 5. **The step-by-step walk.** For each main routine: a numbered list of what it does, in the order of the code. Each step gives the names and addresses. Add pseudo-code when a loop or a condition is not clear from the list.
54
+ 6. **The state machine.** For code that changes from one state to another: a mermaid `stateDiagram-v2` and a transition table. The transition table has the state, the condition, the next state and the code that makes the change.
55
+ 7. **The worked example.** One real case decoded by hand from the image, byte by byte. For example the first bytes of the map of level 1, and the screen cells that they give.
56
+ 8. **Still open.**
57
+
58
+ ```markdown
59
+ ## 3. The state machine of <thing>
60
+
61
+ ~~~mermaid
62
+ stateDiagram-v2
63
+ state "Waiting ($00)" as Wait
64
+ state "Moving ($01)" as Move
65
+ [*] --> Wait
66
+ Wait --> Move: timer = 0
67
+ ~~~
68
+
69
+ | State | Condition | Next state | Code |
70
+ |---|---|---|---|
71
+ | Waiting `$00` | `move_timer` = 0 | Moving `$01` | `update_slider` `$3F8C` |
72
+ ```
73
+
74
+ ## 3. The entity sections
75
+
76
+ A program often has many items of one kind: levels, rooms, enemies, tunes. Give each item one section of the same form, in a page for a group of items. Each section has these parts:
77
+
78
+ 1. **The description.** What the item is, in one paragraph. Embed the image of the item when an asset or the reference has one.
79
+ 2. **The data.** The table rows of the item: its entries in each table, with the values.
80
+ 3. **The setup.** What the init handler of the item does, step by step.
81
+ 4. **The behaviour.** What the frame handler of the item does: a state diagram and a transition table.
82
+ 5. **The links to other items.** The conditions in other items that change this item, for example a door that opens when another room is empty.
83
+
84
+ End the group page with a table of the dependencies between the items.
85
+
86
+ ## 4. The data-format page
87
+
88
+ One page for each format of data, for example a packed map, a music stream or a sprite format. It has these parts:
89
+
90
+ 1. **Where the data is.** The table of pointers, the range of the data, and the routine that reads it.
91
+ 2. **The byte layout.** A table of each byte or bit field, with its meaning. For a stream: each command byte, its arguments and what it does.
92
+ 3. **The decoder.** Pseudo-code of the routine that reads the format.
93
+ 4. **A worked example.** The first bytes of one item, decoded by hand.
94
+ 5. **The assets.** A link to the extracted files and images, for example `../assets/rooms/room00.png`. Embed the image.
95
+
96
+ ```markdown
97
+ | Byte | Meaning |
98
+ |---|---|
99
+ | `$80` | End of the stream. |
100
+ | `$81-$FF` | The next `b & $7F` bytes are cells. |
101
+ | `$01-$7F` | The next byte, `b` times. |
102
+ ```
103
+
104
+ ## 5. The conventions block
105
+
106
+ Put this block near the top of each subsystem page. Change the terms for the program.
107
+
108
+ ```markdown
109
+ **Conventions.**
110
+ - Addresses are C64 addresses. Names are the names of the knowledge.
111
+ - A *pass* is one run of `main_loop` `$1BC2`. A *frame* is one raster interrupt.
112
+ - **Observed** marks a fact that a run showed. **Code** marks a fact from the code only. **Conclusion** marks an interpretation.
113
+ ```
@@ -0,0 +1,43 @@
1
+ # Style of the pages
2
+
3
+ These rules apply to each page that you write. `link.ts` checks the rules that a script can check.
4
+
5
+ ## Names and addresses
6
+
7
+ - Write each routine, table and variable with its name and its address: `` `draw_room` `$09BC` ``. `link.ts` gives the name a link to its line, and the address a link to its entry in the reference.
8
+ - Use the name of the knowledge. Do not make a name that is only in the page. When a part has no good name, record a name with c64-knowledge first.
9
+ - For a place inside a routine, write the name of the routine and the address of the place: `` `draw_room` `$0A04` ``. The address gets a link to its line.
10
+ - For a byte inside a table or a variable, write the name with an offset: `` `room_script_vars+3` `$4E9A` ``.
11
+ - For a value, write an equals sign: `` `scroll_moves` = `$10` ``. Without the sign, `link.ts` reads `$10` as an address.
12
+ - Write addresses with `$` and hex digits in upper case: `$09BC`. Write values in hex with `$`, or in decimal without `$`.
13
+ - Write a range with a hyphen: `` `$4E97-$4E9B` ``.
14
+ - Write the name and the address in backticks. A name without backticks gets no link.
15
+
16
+ ## Evidence
17
+
18
+ - Mark each fact with its evidence, as text:
19
+ - **Observed**: a run showed it.
20
+ - **Code**: the code shows it, and no run showed it.
21
+ - **Conclusion**: an interpretation of the facts.
22
+ - Do not link to the files of a run. The run files are not part of the documentation. Name the run in the text when it helps a reader to do the run again.
23
+ - Do not write a fact that you did not check in the code, the knowledge or a run. Put a question in **Still open**.
24
+
25
+ ## Diagrams and images
26
+
27
+ - Use mermaid for the diagrams: `flowchart` for calls and data flow, `stateDiagram-v2` for state machines. Write the name and the address in each node: `"draw_room $09BC"`.
28
+ - A diagram gets no links. Give the links in a table or a list under the diagram.
29
+ - Keep a diagram small: at most about 20 nodes. Make two diagrams before one diagram that a reader cannot read.
30
+ - Embed an image only from the project. It can be an image of the reference (`reference/images/`) or a PNG that a skill made. It can also be an asset that a script of the project made from a format of the program, for example `../assets/rooms/room00.png`. `link.ts` checks that each image exists.
31
+ - Give each image one line of text that tells what it shows.
32
+
33
+ ## Text
34
+
35
+ - Write short sentences: one fact or one step in each sentence.
36
+ - Use a numbered list for steps in an order. Use a bulleted list for items without an order. Use a table for items with the same fields.
37
+ - Use the same word for the same thing on each page. Define each term of the program one time, in the conventions block.
38
+ - Write the pages for a person who knows the 6502 and the C64, but not the program.
39
+
40
+ ## Generated pages
41
+
42
+ - Do not edit a page in the reference. Change the knowledge, generate the source, and run `generate.ts` again.
43
+ - Do not put a page that you write in the reference directory. `generate.ts` refuses to write when one is there.
@@ -0,0 +1,110 @@
1
+ // c64-docs generate.ts (D80): the reference pages of a program from the
2
+ // knowledge, the source map of c64-source and the image. The pages are
3
+ // generated and never edited; the pages of a person link to them.
4
+
5
+ import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
6
+ import { join, posix } from "node:path";
7
+ import { parseArgs } from "node:util";
8
+
9
+ import { ImageError, readImage } from "#src/host-client/image.ts";
10
+ import { WireFailure } from "#src/host-client/tools.ts";
11
+ import { writeProjectFile } from "#src/host-client/transfer.ts";
12
+ import { conflicts } from "#src/knowledge/conflicts.ts";
13
+ import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
14
+ import { currentRevision, listComments, listModules, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
15
+ import { resolveProjectPath } from "#src/project.ts";
16
+ import { runScript, UsageError } from "#src/script.ts";
17
+ import { buildReference, GENERATED_MARK } from "./lib/reference.ts";
18
+ import { readSourceMap } from "./lib/source-map.ts";
19
+
20
+ const USAGE = `generate.ts --source <dir> --out <dir> [--image <file>]
21
+
22
+ --source <dir> the source directory of c64-source, with its source-map.json,
23
+ relative to the project folder
24
+ --out <dir> the directory for the reference pages, relative to the project
25
+ folder, for example docs/reference; generate.ts writes only
26
+ there and refuses a page in it that it did not write
27
+ --image <file> the image of the source: an image.json of the c64-unpacker
28
+ skill, or a PRG; it decodes the tables and shows the graphics
29
+
30
+ The result is one JSON object.`;
31
+
32
+ /** The files of an earlier run, or a refusal that names a file of a person. */
33
+ function oldFiles(out: string): string[] {
34
+ const directory = resolveProjectPath(out);
35
+ if (!existsSync(directory)) return [];
36
+ const found: string[] = [];
37
+ const walk = (relative: string) => {
38
+ for (const entry of readdirSync(join(directory, relative), { withFileTypes: true })) {
39
+ const path = posix.join(relative, entry.name);
40
+ if (entry.isDirectory()) {
41
+ walk(path);
42
+ continue;
43
+ }
44
+ const generated = path.endsWith(".md") ? readFileSync(join(directory, path), "utf8").startsWith(GENERATED_MARK) : path.startsWith("images/") && path.endsWith(".png");
45
+ if (!generated) throw new WireFailure("invalid-input", `${posix.join(out, path)} is not a page of generate.ts. Nothing was written. Give --out a directory that holds only the reference, and keep the pages of a person outside it.`);
46
+ found.push(path);
47
+ }
48
+ };
49
+ walk("");
50
+ return found;
51
+ }
52
+
53
+ function run(argv: string[]): Record<string, unknown> {
54
+ const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { source: { type: "string" }, out: { type: "string" }, image: { type: "string" }, help: { type: "boolean" } } });
55
+ if (values.help) throw new UsageError("");
56
+ if (positionals.length > 0) throw new UsageError(`generate.ts takes no positional arguments, not ${positionals[0]}`);
57
+ if (values.source === undefined) throw new UsageError("give --source <dir>: the source directory of c64-source");
58
+ if (values.out === undefined) throw new UsageError("give --out <dir>: the directory for the reference pages");
59
+ for (const [option, path] of [["--source", values.source], ["--out", values.out]] as const) {
60
+ try {
61
+ resolveProjectPath(path);
62
+ } catch {
63
+ throw new UsageError(`${option} must be a path relative to the project folder that stays inside it, not ${path}`);
64
+ }
65
+ }
66
+ const map = readSourceMap(values.source);
67
+ let image: ReturnType<typeof readImage> | undefined;
68
+ if (values.image !== undefined) {
69
+ try {
70
+ image = readImage(values.image);
71
+ } catch (error) {
72
+ if (error instanceof ImageError) throw new UsageError(error.message);
73
+ throw error;
74
+ }
75
+ }
76
+ const db = openForRead();
77
+ let knowledge;
78
+ try {
79
+ const found = conflicts(db).length;
80
+ if (found > 0) throw new KnowledgeError("conflict", `The knowledge has ${found} conflict${found === 1 ? "" : "s"} from a git merge. Nothing was written. Run c64-knowledge's knowledge.ts merge, then generate the source and the reference again.`);
81
+ const revision = currentRevision(db);
82
+ // A source of another revision has other lines: the links would point to the wrong code.
83
+ if (map.revision !== revision) {
84
+ throw new WireFailure("invalid-input", `${map.pathOf("source-map.json")} is from knowledge revision ${map.revision}, and the knowledge is at revision ${revision}. Nothing was written. Generate the source again with c64-source, then run generate.ts.`);
85
+ }
86
+ knowledge = {
87
+ revision,
88
+ symbols: listSymbols(db, undefined, undefined, undefined, map.space),
89
+ regions: listRegions(db, undefined, undefined, undefined, map.space),
90
+ comments: listComments(db, undefined, undefined, map.space),
91
+ references: listReferences(db, undefined, undefined, undefined, map.space),
92
+ modules: listModules(db, undefined, undefined, map.space),
93
+ };
94
+ } finally {
95
+ db?.close();
96
+ }
97
+ const old = oldFiles(values.out);
98
+ const built = buildReference({
99
+ out: posix.normalize(values.out),
100
+ map,
101
+ ...knowledge,
102
+ ...(image === undefined ? {} : { image: { path: image.path, byteAt: image.byteAt, inImage: (address: number) => image.segments.some((segment) => address >= segment.start && address <= segment.end) } }),
103
+ });
104
+ const directory = resolveProjectPath(values.out);
105
+ for (const path of old) rmSync(join(directory, path), { force: true });
106
+ for (const file of built.files) writeProjectFile(file.path, file.bytes, { what: "--out" });
107
+ return { out: values.out, revision: knowledge.revision, index: posix.join(posix.normalize(values.out), "index.md"), ...built.counts, image: image?.path ?? "none: give --image to decode the tables and show the graphics" };
108
+ }
109
+
110
+ await runScript(run, { usage: USAGE, refusals: [KnowledgeError] });
@@ -0,0 +1,155 @@
1
+ // The links of a page written by a person (D80). A name in backticks links to
2
+ // its line in the source; the address after it links to the entry of the name
3
+ // in the reference, or, for an address inside the routine or the data of the
4
+ // name ("draw_room $0AAF", a place in draw_room), to the line of that address. A link that points into the source or the reference is
5
+ // managed: link.ts writes it again when a line moves. Other links stay as the
6
+ // person wrote them; link.ts only checks that their targets exist.
7
+
8
+ import { posix } from "node:path";
9
+
10
+ import { anchorsOf, linksOf, proseLines } from "./markdown.ts";
11
+
12
+ export interface LinkContext {
13
+ /** The page, relative to the project folder. */
14
+ page: string;
15
+ /** The address of a name of the knowledge or of the source; undefined for a word that is no name. */
16
+ addressOf: (name: string) => number | undefined;
17
+ /** The project path and line of the label of a name, as path#Lnn. */
18
+ codeOf: (name: string) => string | undefined;
19
+ /** The project path and anchor of the entry of a name, as path#name. */
20
+ entryOf: (name: string) => string | undefined;
21
+ /** The last address of the routine or data of a name: an address up to it is a place inside the name. */
22
+ bodyEnd: (name: string) => number | undefined;
23
+ /** The project path and line that writes an address, as path#Lnn. */
24
+ lineOf: (address: number) => string | undefined;
25
+ /** The name of the routine or data that holds an address, for a message. */
26
+ placeOf: (address: number) => string | undefined;
27
+ /** True for a project path that link.ts manages: a file of the source or a page of the reference. */
28
+ managed: (path: string) => boolean;
29
+ }
30
+
31
+ export type ProblemKind = "unknown-name" | "wrong-address" | "missing-file" | "missing-anchor" | "missing-line" | "outside-project" | "stale-reference";
32
+
33
+ export interface Problem {
34
+ page: string;
35
+ /** 1 is the first line. */
36
+ line: number;
37
+ kind: ProblemKind;
38
+ message: string;
39
+ }
40
+
41
+ const NAME = String.raw`([A-Za-z_][A-Za-z0-9_]*)(\+(?:\$[0-9A-Fa-f]{1,4}|[0-9]+))?`;
42
+ const ADDRESS = String.raw`\$([0-9A-Fa-f]{2,4})(?:-\$[0-9A-Fa-f]{2,4})?`;
43
+ const TICK = "`";
44
+ /**
45
+ * A name in backticks, linked or not, with an optional address in backticks
46
+ * after it, linked or not. Groups: 1 the [ of a linked name, 2 the name, 3 its
47
+ * offset, 4 the target of the name, 5 the space or bracket between them, 6 the
48
+ * address text, 7 its start digits, 8 the target of the address.
49
+ */
50
+ const MENTION = new RegExp(
51
+ String.raw`(\[)?${TICK}${NAME}${TICK}(?:\]\(([^)\s]*)\))?(?:(\s*\(\s*|\s+)\[?${TICK}(${ADDRESS})${TICK}(?:\]\(([^)\s]*)\))?)?`,
52
+ "g",
53
+ );
54
+
55
+ const relative = (page: string, target: string) => {
56
+ const [path, fragment] = target.split("#");
57
+ const shown = posix.relative(posix.dirname(page), path!) || posix.basename(path!);
58
+ return fragment === undefined ? shown : `${shown}#${fragment}`;
59
+ };
60
+ /** The project path of a link target, without its fragment; undefined for a link out of the project (a URL, or a page anchor only). */
61
+ export function targetPath(page: string, target: string): string | undefined {
62
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("#")) return undefined;
63
+ return posix.normalize(posix.join(posix.dirname(page), decodeURIComponent(target.split("#")[0]!)));
64
+ }
65
+
66
+ /** Writes the links of the name mentions of a page; returns the new text, the number of links changed and the problems. */
67
+ export function linkPage(text: string, context: LinkContext): { text: string; changed: number; problems: Problem[] } {
68
+ const problems: Problem[] = [];
69
+ let changed = 0;
70
+ const lines = proseLines(text).map((line, index) => {
71
+ if (!line.prose) return line.text;
72
+ return line.text.replace(MENTION, (whole: string, open: string | undefined, name: string, offset: string | undefined, nameTarget: string | undefined, space: string | undefined, addressText: string | undefined, digits: string | undefined, addressTarget: string | undefined, at: number, all: string) => {
73
+ // "[`x`]" without a target is no link of ours: leave it.
74
+ if (open !== undefined && nameTarget === undefined) return whole;
75
+ const ownLink = (target: string | undefined) => target !== undefined && !context.managed(targetPath(context.page, target) ?? "");
76
+ const address = context.addressOf(name);
77
+ const report = (kind: ProblemKind, message: string) => problems.push({ page: context.page, line: index + 1, kind, message });
78
+ // In a list "`a`/`b` `$1000`/`$1010`" the address belongs to another name: the name gets its link, the address stays as it is.
79
+ const namePart = open === undefined ? `${TICK}${name}${offset ?? ""}${TICK}` : `[${TICK}${name}${offset ?? ""}${TICK}](${nameTarget})`;
80
+ const listed = addressText !== undefined && (all[at - 1] === "/" || all[at + whole.length] === "/");
81
+ const rest = listed ? whole.slice(namePart.length) : "";
82
+ const claimed = addressText !== undefined;
83
+ if (listed) addressText = digits = addressTarget = undefined;
84
+ if (address === undefined) {
85
+ // A word in backticks is no claim of a name; with an address after it, or with a link of ours, it is.
86
+ if (claimed || (nameTarget !== undefined && !ownLink(nameTarget))) {
87
+ report("unknown-name", `\`${name}\` is no name of the knowledge or the source. Apply the name to the knowledge, or correct the page.`);
88
+ }
89
+ return whole;
90
+ }
91
+ const delta = offset === undefined ? 0 : offset.startsWith("+$") ? Number.parseInt(offset.slice(2), 16) : Number.parseInt(offset.slice(1), 10);
92
+ const given = digits === undefined ? undefined : Number.parseInt(digits, 16);
93
+ const place = given !== undefined && given !== address + delta && given > address && given <= (context.bodyEnd(name) ?? address);
94
+ if (given !== undefined && given !== address + delta && !place) {
95
+ const expected = `$${(address + delta).toString(16).toUpperCase().padStart(4, "0")}`;
96
+ const holder = context.placeOf(given!);
97
+ report("wrong-address", `\`${name}${offset ?? ""}\` is ${expected} in the knowledge, and the page gives ${addressText}${holder === undefined || holder === name ? "" : `, a place in \`${holder}\``}. Correct the name or the address.${digits!.length <= 2 ? ` For a value, write \`${name}\` = \`${addressText}\`.` : ""}`);
98
+ }
99
+ const shownName = `${TICK}${name}${offset ?? ""}${TICK}`;
100
+ const code = context.codeOf(name);
101
+ const nameLink = ownLink(nameTarget) ? `[${shownName}](${nameTarget})` : code === undefined ? shownName : `[${shownName}](${relative(context.page, code)})`;
102
+ let result = nameLink;
103
+ if (addressText !== undefined) {
104
+ const entry = place ? context.lineOf(given!) : context.entryOf(name);
105
+ const shownAddress = `${TICK}${addressText}${TICK}`;
106
+ result += space! + (ownLink(addressTarget) ? `[${shownAddress}](${addressTarget})` : entry === undefined ? shownAddress : `[${shownAddress}](${relative(context.page, entry)})`);
107
+ }
108
+ result += rest;
109
+ if (result !== whole) changed++;
110
+ return result;
111
+ });
112
+ });
113
+ return { text: lines.join("\n"), changed, problems };
114
+ }
115
+
116
+ /** The files that a check reads: their text, or undefined when the file does not exist. */
117
+ export interface Files {
118
+ exists: (path: string) => boolean;
119
+ /** The text of a project file, or undefined when it is no file. */
120
+ text: (path: string) => string | undefined;
121
+ inside: (path: string) => boolean;
122
+ }
123
+
124
+ /** Checks that each link and image of a page reaches a file, and its anchor or line. */
125
+ export function checkLinks(page: string, text: string, files: Files): Problem[] {
126
+ const problems: Problem[] = [];
127
+ const anchors = new Map<string, Set<string>>();
128
+ const anchorsAt = (path: string) => {
129
+ if (!anchors.has(path)) anchors.set(path, anchorsOf(path === page ? text : (files.text(path) ?? "")));
130
+ return anchors.get(path)!;
131
+ };
132
+ for (const link of linksOf(text)) {
133
+ const report = (kind: ProblemKind, message: string) => problems.push({ page, line: link.line, kind, message });
134
+ const fragment = link.target.includes("#") ? link.target.slice(link.target.indexOf("#") + 1) : undefined;
135
+ if (/^[a-z][a-z0-9+.-]*:/i.test(link.target)) continue;
136
+ const path = link.target.startsWith("#") ? page : targetPath(page, link.target)!;
137
+ if (!files.inside(path)) {
138
+ report("outside-project", `${link.target} leaves the project folder.`);
139
+ continue;
140
+ }
141
+ if (!files.exists(path)) {
142
+ report("missing-file", `${link.target}: there is no ${path}.`);
143
+ continue;
144
+ }
145
+ if (fragment === undefined || fragment === "") continue;
146
+ const line = /^L(\d+)(?:-L?\d+)?$/.exec(fragment);
147
+ if (path.endsWith(".md")) {
148
+ if (!anchorsAt(path).has(fragment)) report("missing-anchor", `${link.target}: ${path} has no anchor ${fragment}.`);
149
+ } else if (line !== null) {
150
+ const count = (files.text(path) ?? "").split("\n").length;
151
+ if (Number(line[1]) > count) report("missing-line", `${link.target}: ${path} has ${count} lines.`);
152
+ }
153
+ }
154
+ return problems;
155
+ }