@henols/vice-mcp 0.2.2 → 0.2.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.
Files changed (84) hide show
  1. package/README.md +2 -2
  2. package/THIRD-PARTY-NOTICES.md +422 -1
  3. package/anno-bank.ts +171 -0
  4. package/anno-cli.ts +1736 -163
  5. package/anno-confidence.ts +2 -2
  6. package/anno-derive.ts +6 -6
  7. package/anno-details.ts +4 -4
  8. package/anno-enum-gen.ts +416 -30
  9. package/anno-export-asm.ts +1211 -126
  10. package/anno-graphics.ts +338 -0
  11. package/anno-hazard-report.ts +1367 -0
  12. package/anno-import.ts +495 -0
  13. package/anno-index.ts +8 -8
  14. package/anno-join.ts +480 -0
  15. package/anno-memmap-render.ts +22 -21
  16. package/anno-provenance-ledger.ts +472 -0
  17. package/anno-regbits-gen.ts +13 -13
  18. package/anno-register.ts +159 -0
  19. package/anno-store-export.ts +661 -0
  20. package/anno-store.ts +635 -124
  21. package/anno-symbols.ts +7 -7
  22. package/anno-tools.ts +1169 -16
  23. package/anno-types.ts +313 -40
  24. package/backend-detect.mts +124 -312
  25. package/build.ts +3 -1
  26. package/capture-predicate.ts +597 -0
  27. package/channel-lock.ts +349 -0
  28. package/evid-ingest.ts +217 -0
  29. package/evid-reconcile.ts +316 -0
  30. package/host-tool-client.ts +430 -0
  31. package/incident-record.ts +23 -12
  32. package/install-resources.ts +29 -13
  33. package/memmap-lookup.ts +285 -0
  34. package/package.json +27 -8
  35. package/prg-image.ts +1 -2
  36. package/repo-root.ts +87 -3
  37. package/resources/backend-detect.mjs +98 -236
  38. package/resources/broker-control.mjs +220 -54
  39. package/resources/broker-epoch.mjs +7 -8
  40. package/resources/broker-kill.mjs +36 -31
  41. package/resources/broker-launch.mjs +511 -374
  42. package/resources/broker-state.mjs +69 -24
  43. package/resources/container-guard.mjs +1 -1
  44. package/resources/ghidra-project.mjs +790 -0
  45. package/resources/host-tool.mjs +2533 -0
  46. package/resources/vice-broker.mjs +434 -290
  47. package/resources/vice-launcher.sh +127 -9
  48. package/stock-address.ts +1 -1
  49. package/stock-condition.ts +1 -1
  50. package/stock-connect.ts +9 -5
  51. package/stock-derived.ts +29 -37
  52. package/stock-diagnose.ts +200 -36
  53. package/stock-dispatch.ts +179 -77
  54. package/stock-handler.ts +1 -1
  55. package/stock-paths.ts +18 -14
  56. package/stock-petscii.ts +1 -1
  57. package/stock-protocol.ts +1 -1
  58. package/stock-recycle.ts +83 -2
  59. package/stock-reproducible-run.ts +811 -0
  60. package/stock-run-until.ts +100 -1
  61. package/stock-symbols.ts +4 -4
  62. package/stock-timing.ts +1 -1
  63. package/stop-oracle.ts +167 -0
  64. package/text-capability-probe.ts +660 -0
  65. package/text-connect.ts +157 -0
  66. package/text-protocol.ts +810 -0
  67. package/text-tools.ts +778 -0
  68. package/textmon-backtrace.ts +385 -0
  69. package/textmon-cpuhistory.ts +335 -0
  70. package/textmon-memmap.ts +494 -0
  71. package/textmon-profile.ts +458 -0
  72. package/textmon-registers.ts +748 -0
  73. package/tools-manifest.stock.json +864 -3
  74. package/vice-broker-client.ts +253 -108
  75. package/vice-errors.ts +268 -0
  76. package/vice-proxy.ts +339 -2144
  77. package/vsf-slice.ts +640 -0
  78. package/anno-d64.ts +0 -310
  79. package/capability-registry.ts +0 -390
  80. package/refresh-manifest.ts +0 -124
  81. package/tools-manifest.json +0 -1223
  82. package/vice-probe.ts +0 -278
  83. package/vice-sync.ts +0 -336
  84. package/vice.ts +0 -772
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+ // memmap-lookup.ts
3
+ //
4
+ // Phase 37, plan 37-01 (IMP-01/AUTO-01's shared foundation): the ONE loader
5
+ // for `c64-memory-mapping`'s own `memmap.json`, its content digest, and
6
+ // narrowest-containing-range selection over its 959 entries.
7
+ //
8
+ // THIS MODULE MUST NEVER IMPORT `hostpath.ts` OR `containerpath.ts` -- every
9
+ // path here is repo-relative and derived from this module's own location, the
10
+ // same posture `anno-regbits-gen.ts` and `dxa-blocks.ts` take for themselves.
11
+ // It also NEVER NAMES `node:sqlite` and NEVER OPENS THE ANNOTATION STORE:
12
+ // `anno-store.ts` is the one module `anno-seam.test.ts` allows to name that
13
+ // dependency, and this module answers a pure question about a static JSON
14
+ // file that has nothing to do with the store's own persistence.
15
+ //
16
+ // D-37-03: `anno-regbits-gen.ts:62` already computes the SAME `HERE`-relative
17
+ // five-hop path to `memmap.json` and `anno-regbits-gen.ts:377` already
18
+ // computes its sha256 digest, for a different purpose (bit-name generation).
19
+ // This module COPIES that formula rather than importing the generator --
20
+ // `anno-regbits-gen.ts`'s own header declares itself "the sole read of
21
+ // c64-memory-mapping's own memmap.json ... for this purpose", and widening
22
+ // that consumer set is a separate decision this plan does not make.
23
+ //
24
+ // WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR: loading and caching
25
+ // `memmap.json`'s 959 entries, computing the file's own sha256 provenance
26
+ // digest, and the NARROWEST-CONTAINING-RANGE selection rule an address
27
+ // resolves through. `anno-join.ts` calls this module; nothing else needs to.
28
+ //
29
+ // WHAT NOT TO DO:
30
+ // - Never re-derive `MEMMAP_PATH` from a different relative offset. This
31
+ // formula has already broken once when the skills tree moved (plan
32
+ // 16-01's own deviation note) -- one HERE-relative constant, copied
33
+ // verbatim in shape from `anno-regbits-gen.ts:62`.
34
+ // - Never mutate the cached entries array. `loadMemmap()` returns the same
35
+ // frozen array on every call after the first; a caller that needs a
36
+ // filtered view copies it.
37
+ // - Never derive "is this range bank-conditional" from `memmap.json`'s own
38
+ // fields. `BANK_CONDITIONAL_RANGES` below is HAND-MAINTAINED on purpose
39
+ // (37-RESEARCH.md Pitfall 2) -- the schema carries no structured
40
+ // bank-condition field, only free prose inside `desc`.
41
+
42
+ import { createHash } from "node:crypto";
43
+ import { readFileSync } from "node:fs";
44
+ import { dirname, join } from "node:path";
45
+ import { fileURLToPath } from "node:url";
46
+
47
+ const HERE = dirname(fileURLToPath(import.meta.url));
48
+
49
+ /** The `HERE`-relative path to `c64-memory-mapping`'s `memmap.json`, using the
50
+ * SAME five-hop formula `anno-regbits-gen.ts:62` uses. Copied, not imported
51
+ * (D-37-03). */
52
+ export const MEMMAP_PATH = join(HERE, "..", "..", "..", "src", "skills", "c64-memory-mapping", "memmap.json");
53
+
54
+ /** One entry as `memmap.json` holds it. MEASURED at plan time: 959 entries,
55
+ * `start`/`end` are INCLUSIVE integers with no length field, and `sym` is
56
+ * present on 219 of them. */
57
+ export interface MemmapEntry {
58
+ start: number;
59
+ end: number;
60
+ label: string;
61
+ section: string;
62
+ desc: string;
63
+ src: string;
64
+ sym?: string;
65
+ }
66
+
67
+ interface MemmapDocument {
68
+ sources: unknown[];
69
+ entries: MemmapEntry[];
70
+ }
71
+
72
+ /** Module-level cache, exactly as `anno-enum-gen.ts`'s `loadRegBits()` caches
73
+ * its own generated table -- `memmap.json` is read and parsed once per
74
+ * process, never per call. */
75
+ let cachedEntries: readonly MemmapEntry[] | undefined;
76
+
77
+ /** Reads and parses `memmap.json`, caching the parsed array. Refuses BY NAME
78
+ * if the top-level shape is not `{sources, entries}` or if `entries` is
79
+ * empty -- a malformed or truncated memmap.json must not read as "a memmap
80
+ * with nothing in it". */
81
+ export function loadMemmap(): readonly MemmapEntry[] {
82
+ if (cachedEntries !== undefined) return cachedEntries;
83
+
84
+ let parsed: unknown;
85
+ try {
86
+ parsed = JSON.parse(readFileSync(MEMMAP_PATH, "utf8"));
87
+ } catch (err) {
88
+ const reason = err instanceof Error ? err.message : String(err);
89
+ throw new Error(`memmap-lookup: ${MEMMAP_PATH} could not be read/parsed as JSON (${reason}).`);
90
+ }
91
+
92
+ const doc = parsed as Partial<MemmapDocument> | null;
93
+ if (doc === null || typeof doc !== "object" || !Array.isArray(doc.sources) || !Array.isArray(doc.entries)) {
94
+ throw new Error(
95
+ `memmap-lookup: ${MEMMAP_PATH} is not a { sources, entries } document -- refusing to guess a shape for a ` +
96
+ "malformed memmap.json.",
97
+ );
98
+ }
99
+ if (doc.entries.length === 0) {
100
+ throw new Error(
101
+ `memmap-lookup: ${MEMMAP_PATH}'s "entries" array is empty -- "a memmap with nothing in it" and "a memmap ` +
102
+ 'that failed to load" must not read the same, so this refuses rather than returning an empty selector.',
103
+ );
104
+ }
105
+
106
+ cachedEntries = Object.freeze(doc.entries.slice());
107
+ return cachedEntries;
108
+ }
109
+
110
+ /** The sha256 hex digest of `memmap.json`'s RAW BYTES -- the provenance value
111
+ * `AUTO-08` (a later plan) appends to derived comment text. MEASURED at plan
112
+ * time: this equals the same value `anno-regbits.json`'s committed banner
113
+ * carries (`_generated.memmapSha256`), and the test that pins this asserts
114
+ * that RELATION rather than a hard-coded literal, because the map is a living
115
+ * file. */
116
+ export function memmapDigest(): string {
117
+ return createHash("sha256").update(readFileSync(MEMMAP_PATH)).digest("hex");
118
+ }
119
+
120
+ /**
121
+ * D-37-13: the fixed, machine-parseable prefix `anno-join.ts` appends
122
+ * `memmapDigest()`'s full 64-character lowercase hex digest to, on every
123
+ * derived comment (`AUTO-08`). Chosen to be unlikely to collide with
124
+ * ordinary comment prose and to sit LAST in the comment text (no closing
125
+ * delimiter follows it), so a regex can find it without knowing the
126
+ * preceding label's own shape: `new RegExp(PROVENANCE_TOKEN_PREFIX_ESCAPED +
127
+ * "[0-9a-f]{64}$")`. Exported from here, not `anno-join.ts`, because the
128
+ * digest and the prefix that names it belong to the same module as
129
+ * `memmapDigest()` itself.
130
+ */
131
+ export const PROVENANCE_TOKEN_PREFIX = "[memmap-sha256:";
132
+
133
+ /** WHICH of the three steps decided a selection, or `unique` when only one
134
+ * entry contained the address at all. `AUTO-02` names two steps
135
+ * (narrowest-range-wins, then the `sym` tie-break); D-37-10 adds a third,
136
+ * because the phase's own headline `$D020` example is NOT resolved by the
137
+ * first two -- MEASURED at plan time, its two 1-byte contenders both lack a
138
+ * `sym`. Reported rather than left for a caller to infer, so a test (and
139
+ * plan 37-04's controls) can assert WHICH rule decided, not merely what it
140
+ * decided. */
141
+ export type MemmapTieBreak = "unique" | "width" | "symbol" | "order";
142
+
143
+ /** What `selectMemmapEntry()` returns for a unique or a resolved-tie hit.
144
+ * `contenderCount` is the number of containing entries the scan considered,
145
+ * so a caller can tell a unique hit from a resolved tie without re-running
146
+ * the scan itself. */
147
+ export interface MemmapSelection {
148
+ entry: MemmapEntry;
149
+ width: number;
150
+ contenderCount: number;
151
+ tieBrokenBy: MemmapTieBreak;
152
+ }
153
+
154
+ /** Inclusive width of one entry: `end - start`. Named once so every step
155
+ * below computes it identically. */
156
+ function inclusiveWidth(entry: MemmapEntry): number {
157
+ return entry.end - entry.start;
158
+ }
159
+
160
+ /**
161
+ * STEP ONE, WIDTH -- the whole of narrowest-range-wins. Prefers the smallest
162
+ * `end - start`; returns every entry tied at that minimum, since a single
163
+ * winner here is `unique`ly correct only when nothing else shares its width.
164
+ * Kept as its own named function (not folded into a single comparator) so
165
+ * plan 37-04's control -- "switch selection to first-match" -- is a single,
166
+ * small, textual replacement of exactly this step, per this plan's own
167
+ * `<read_first>` instruction.
168
+ */
169
+ function narrowestWidthSurvivors(containing: readonly MemmapEntry[]): MemmapEntry[] {
170
+ let minWidth = Infinity;
171
+ for (const entry of containing) {
172
+ const width = inclusiveWidth(entry);
173
+ if (width < minWidth) minWidth = width;
174
+ }
175
+ return containing.filter((entry) => inclusiveWidth(entry) === minWidth);
176
+ }
177
+
178
+ /**
179
+ * STEP TWO, SYMBOL -- `AUTO-02`'s own tie-break. Among step one's survivors,
180
+ * prefers an entry carrying a non-empty `sym` over one that does not.
181
+ * D-37-11's `$0000` fixture (three equal-width contenders, exactly one
182
+ * carrying `sym: "D6510"`) is this step's own fixture. Returns every
183
+ * `sym`-carrying survivor when at least one exists, else returns every
184
+ * survivor unchanged (this step decided nothing -- step three must run).
185
+ */
186
+ function symbolSurvivors(survivors: readonly MemmapEntry[]): MemmapEntry[] {
187
+ const withSym = survivors.filter((entry) => typeof entry.sym === "string" && entry.sym.length > 0);
188
+ return withSym.length > 0 ? withSym : survivors.slice();
189
+ }
190
+
191
+ /**
192
+ * STEP THREE, ORDER -- D-37-10. Among step two's survivors, the entry
193
+ * appearing FIRST in `memmap.json`'s own `entries` array wins. Stated
194
+ * explicitly, rather than left to whatever order a scan happened to
195
+ * produce, because the phase's own headline `$D020` example needs it: two
196
+ * 1-byte contenders there are tied on width AND neither carries a `sym`, so
197
+ * without this named third rule the selection would be an unstated,
198
+ * scan-order accident that a later refactor could change silently. No
199
+ * `sort()` is used here -- a single linear scan over `entries` (the same
200
+ * order the caller supplied) finds the first survivor, which is cheaper and
201
+ * keeps this step's own mutation (plan 37-05's "reverse the tie-break")
202
+ * a one-line replacement rather than a sort-comparator edit.
203
+ */
204
+ function orderWinner(survivors: readonly MemmapEntry[], entries: readonly MemmapEntry[]): MemmapEntry {
205
+ for (const entry of entries) {
206
+ if (survivors.includes(entry)) return entry;
207
+ }
208
+ // Unreachable: `survivors` is always drawn from `entries` by reference, so
209
+ // the scan above always finds one before falling through.
210
+ return survivors[0]!;
211
+ }
212
+
213
+ /**
214
+ * The THREE-DEEP selection order (D-37-10): narrowest-containing-range,
215
+ * then the `sym` tie-break (`AUTO-02`), then -- because those two do not
216
+ * resolve every real tie in the committed `memmap.json` -- the stated
217
+ * residual rule of "first in `entries` order". Collects every entry whose
218
+ * inclusive `[start, end]` contains `address`; returns `undefined` if none
219
+ * does. `tieBrokenBy` names WHICH step decided, so a caller (and plan
220
+ * 37-04's controls) can tell width-decided from symbol-decided from
221
+ * order-decided, not merely infer it from the winning entry's own shape.
222
+ */
223
+ export function selectMemmapEntry(address: number, entries: readonly MemmapEntry[] = loadMemmap()): MemmapSelection | undefined {
224
+ const containing: MemmapEntry[] = [];
225
+ for (const entry of entries) {
226
+ if (address < entry.start || address > entry.end) continue;
227
+ containing.push(entry);
228
+ }
229
+ const contenderCount = containing.length;
230
+ if (contenderCount === 0) return undefined;
231
+ if (contenderCount === 1) {
232
+ const entry = containing[0]!;
233
+ return { entry, width: inclusiveWidth(entry), contenderCount, tieBrokenBy: "unique" };
234
+ }
235
+
236
+ const widthSurvivors = narrowestWidthSurvivors(containing);
237
+ if (widthSurvivors.length === 1) {
238
+ const entry = widthSurvivors[0]!;
239
+ return { entry, width: inclusiveWidth(entry), contenderCount, tieBrokenBy: "width" };
240
+ }
241
+
242
+ const symSurvivors = symbolSurvivors(widthSurvivors);
243
+ if (symSurvivors.length === 1) {
244
+ const entry = symSurvivors[0]!;
245
+ return { entry, width: inclusiveWidth(entry), contenderCount, tieBrokenBy: "symbol" };
246
+ }
247
+
248
+ const entry = orderWinner(symSurvivors, entries);
249
+ return { entry, width: inclusiveWidth(entry), contenderCount, tieBrokenBy: "order" };
250
+ }
251
+
252
+ /** One hand-maintained bank-conditional range: its meaning depends on the
253
+ * processor port's bits #0-#2 at `$0001`. */
254
+ export interface BankConditionalRange {
255
+ start: number;
256
+ end: number;
257
+ why: string;
258
+ }
259
+
260
+ /**
261
+ * The three inclusive ranges whose interpretation depends on the processor
262
+ * port, HAND-MAINTAINED rather than derived from `memmap.json` (37-RESEARCH.md
263
+ * Pitfall 2): only 6 of 959 entries even mention the condition in free-text
264
+ * `desc`, and there is no structured bank-condition field to scan instead.
265
+ * Nothing in THIS module branches on this list -- a later plan's bank-decode
266
+ * logic does. Landed here so the list has one home rather than being
267
+ * reinvented per caller.
268
+ */
269
+ export const BANK_CONDITIONAL_RANGES: readonly BankConditionalRange[] = Object.freeze([
270
+ {
271
+ start: 0xa000,
272
+ end: 0xbfff,
273
+ why: "BASIC ROM when banked in, otherwise RAM (bits #0-#2 of $0001 select between them).",
274
+ },
275
+ {
276
+ start: 0xd000,
277
+ end: 0xdfff,
278
+ why: "I/O area, Character ROM, or RAM depending on $0001 bits #0-#2 -- the canonical three-way case.",
279
+ },
280
+ {
281
+ start: 0xe000,
282
+ end: 0xffff,
283
+ why: "KERNAL ROM when banked in, otherwise RAM (bits #0-#2 of $0001 select between them).",
284
+ },
285
+ ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henols/vice-mcp",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "VICE emulator MCP server for C64 reverse-engineering: a stdio MCP server that proxies vice tools to a host VICE MCP server.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,23 +9,24 @@
9
9
  "main": "vice-proxy.ts",
10
10
  "files": [
11
11
  "vice-proxy.ts",
12
- "vice.ts",
13
- "vice-sync.ts",
14
- "vice-probe.ts",
12
+ "vice-errors.ts",
15
13
  "vice-broker-client.ts",
14
+ "host-tool-client.ts",
16
15
  "version.ts",
17
16
  "stock-protocol.ts",
18
17
  "stock-connect.ts",
18
+ "text-protocol.ts",
19
+ "text-connect.ts",
20
+ "text-tools.ts",
21
+ "channel-lock.ts",
19
22
  "containerpath.ts",
20
23
  "hostpath.ts",
21
24
  "repo-root.ts",
22
25
  "install-resources.ts",
23
26
  "incident-record.ts",
24
- "refresh-manifest.ts",
25
27
  "build.ts",
26
28
  "container-guard.mts",
27
29
  "backend-detect.mts",
28
- "capability-registry.ts",
29
30
  "stock-dispatch.ts",
30
31
  "stock-derived.ts",
31
32
  "stock-handler.ts",
@@ -44,6 +45,12 @@
44
45
  "disasm-opcodes.ts",
45
46
  "disasm-decoder.ts",
46
47
  "disasm-renderer.ts",
48
+ "textmon-memmap.ts",
49
+ "textmon-cpuhistory.ts",
50
+ "textmon-backtrace.ts",
51
+ "textmon-profile.ts",
52
+ "textmon-registers.ts",
53
+ "text-capability-probe.ts",
47
54
  "stock-memory-search.ts",
48
55
  "stock-symbols.ts",
49
56
  "stock-vicii.ts",
@@ -51,9 +58,9 @@
51
58
  "stock-sprites.ts",
52
59
  "stock-timing.ts",
53
60
  "stock-run-until.ts",
61
+ "stock-reproducible-run.ts",
54
62
  "stock-diagnose.ts",
55
63
  "stock-recycle.ts",
56
- "anno-d64.ts",
57
64
  "anno-cli.ts",
58
65
  "anno-symbols.ts",
59
66
  "anno-regbits-gen.ts",
@@ -63,18 +70,30 @@
63
70
  "anno-confidence.ts",
64
71
  "anno-memmap-render.ts",
65
72
  "anno-export-asm.ts",
73
+ "anno-provenance-ledger.ts",
66
74
  "anno-coverage.ts",
67
75
  "block-class.ts",
76
+ "anno-hazard-report.ts",
77
+ "evid-reconcile.ts",
78
+ "evid-ingest.ts",
68
79
  "prg-image.ts",
80
+ "vsf-slice.ts",
81
+ "capture-predicate.ts",
82
+ "stop-oracle.ts",
69
83
  "anno-types.ts",
70
84
  "anno-index.ts",
71
85
  "anno-store.ts",
86
+ "anno-store-export.ts",
72
87
  "anno-tools.ts",
73
88
  "anno-derive.ts",
74
89
  "anno-details.ts",
75
90
  "anno-register.ts",
91
+ "anno-import.ts",
92
+ "anno-join.ts",
93
+ "anno-bank.ts",
94
+ "anno-graphics.ts",
95
+ "memmap-lookup.ts",
76
96
  "resources",
77
- "tools-manifest.json",
78
97
  "tools-manifest.stock.json",
79
98
  "README.md",
80
99
  "THIRD-PARTY-NOTICES.md"
package/prg-image.ts CHANGED
@@ -36,8 +36,7 @@
36
36
  // census) imports `decodeRawData`, `parsePrg` and `flatImageOrigin` for the
37
37
  // same three facts on the CLI's route. `scripts/check-npm-packages.mjs` walks
38
38
  // that closure over `files[]` and fails the pack the moment a reachable module
39
- // sits outside the listed set, exactly as `anno-d64.ts`'s own header records
40
- // for the same reason.
39
+ // sits outside the listed set.
41
40
  //
42
41
  // CORRECTED 2026-08-30 (WR-07, plan 29-16). This paragraph previously named
43
42
  // `anno-cli.ts` as importing `parsePrg` and `flatImageOrigin` and rested the
package/repo-root.ts CHANGED
@@ -182,11 +182,95 @@ export function repoRoot({ from = HERE, env = process.env, exists = existsSync }
182
182
  return resolve(from, "..", "..", "..");
183
183
  }
184
184
 
185
+ /** The ONE definition of the tool-written root every writer in this codebase
186
+ * ultimately derives its location from (D-33, 2026-09-08 clean-break
187
+ * consolidation): `join(repoRoot(...), ".c64-re-tools")`.
188
+ *
189
+ * CORRECTED 2026-09-08 (gap `G-40-1`; see
190
+ * .planning/notes/ghidra-dot-path-check-semantics.md): this comment used to
191
+ * claim (a) that the Ghidra runs directory was among the writers resolving
192
+ * through THIS function, and (b) that the literal string below had exactly
193
+ * one non-comment occurrence in the codebase. Both were false when written,
194
+ * and neither was ever measured before being written down. The corrected
195
+ * picture:
196
+ *
197
+ * - FIVE files call `toolsDir()`/`supervisorDir()` directly, across FOUR
198
+ * distinct subdirectories: incident-record.ts (`incidents`),
199
+ * stock-paths.ts (`snapshots`), vice-proxy.ts (`bin`, reading back the
200
+ * deployed launcher path), and vice.ts + vice-broker-client.ts, both via
201
+ * `supervisorDir()` (`supervisor`, the broker state directory -- one
202
+ * writer, two readers).
203
+ * - FOUR files cannot import this container-side module at all, so each
204
+ * joins `".c64-re-tools"` with its own trailing segment(s) directly,
205
+ * matching this function's shape by CONVENTION, never by shared code:
206
+ * install-resources.ts's `installTargetDir()` (`bin` -- module-cycle
207
+ * avoidance, since THIS file's own bottom-of-module call invokes it),
208
+ * vice-broker.mts's `parseArgs()` state-dir fallback (`supervisor` --
209
+ * host-bound, compiled separately by build.ts), host-tool.mts's
210
+ * `oracle.run` scratch directory (`runs/oracle` -- host-bound), and
211
+ * ghidra-project.mts's `ghidraRunsRoot()`/`ghidraRunsRealRoot()`
212
+ * (`runs/ghidra` -- host-bound, reached through a symlinked alias
213
+ * handle, see below). Every one of these four must keep its literal
214
+ * equal to `join(toolsDir(...), <same segments>)`, by convention, or the
215
+ * two halves of this codebase silently disagree on where the root is.
216
+ *
217
+ * The Ghidra runs root is emphatically NOT, and never was, one of the five
218
+ * files that call this function directly -- it is host-bound
219
+ * (ghidra-project.mts) and cannot import this file at all.
220
+ *
221
+ * The literal string ".c64-re-tools" therefore has exactly 6 non-comment
222
+ * occurrences in this codebase, across 5 files. repo-root.test.ts's census
223
+ * gate reads BOTH the count and this file list straight out of this
224
+ * sentence and the bullet list below -- never duplicated by hand a second
225
+ * time in the test -- and compares both against the real tree, with a
226
+ * planted-violation control proving the comparison predicate actually
227
+ * fires. That is the mechanism that stops this specific claim going false
228
+ * again (threat `T-40-10-02`):
229
+ * - repo-root.ts -- this definition, the line below (1)
230
+ * - install-resources.ts -- `installTargetDir()`'s `bin` join (1)
231
+ * - vice-broker.mts -- `parseArgs()`'s state-dir fallback, BOTH branches
232
+ * of one ternary on the same line (2)
233
+ * - ghidra-project.mts -- `GHIDRA_RUNS_HANDLE_TARGET`, the alias handle's
234
+ * relative symlink target (1)
235
+ * - host-tool.mts -- `oracle.run`'s scratch-directory join (1)
236
+ *
237
+ * The Ghidra alias handle: a non-dotted sibling of this root
238
+ * (`<repoRoot>/c64-re-tools`, no leading dot), a symlink whose RELATIVE
239
+ * target is this root's own directory name, minted host-side by the broker
240
+ * at startup (vice-broker.mts) and re-asserted, idempotently, as a
241
+ * precondition by ghidra-project.mts's `ensureGhidraRunsHandle()` on every
242
+ * resolve -- refused BY NAME, never repaired, when something unexpected
243
+ * already sits at the handle path. It exists because Ghidra's own
244
+ * project-location refusal binds the ABSOLUTIZED path argument it is
245
+ * handed (`ProjectLocator` calls `java.io.File.getAbsolutePath()`, never
246
+ * `getCanonicalPath()` -- MEASURED from the class's own bytecode -- so it
247
+ * absolutizes a relative argument but does not resolve a symlink), so a
248
+ * tool that refuses a dot-prefixed segment in the path it is HANDED can
249
+ * still be pointed, indirectly, at bytes that live physically inside this
250
+ * one root. The superseded method that produced the original overstated
251
+ * claim was running this project's OWN dot-segment-refusal check
252
+ * (`hasDotPrefixedSegment()`) against a synthetic string -- which observes
253
+ * this project, never Ghidra. See
254
+ * .planning/notes/ghidra-dot-path-check-semantics.md for the full live
255
+ * measurement against real Ghidra 12.1.3.
256
+ *
257
+ * This is a clean break, not a migration: no code path falls back to any of
258
+ * the five previous locations when the new one is absent, and there is no
259
+ * opt-back-in environment variable. A pre-existing tree at one of the old
260
+ * locations is simply left on disk, unread, for the user to delete by hand.
261
+ * `VICE_POOL_DIR` / `VICE_EPOCH_FILE` / `VICE_SUPERVISOR_DIR` /
262
+ * `VICE_INCIDENTS_DIR` are unaffected by this move -- they still override
263
+ * their respective resolved default, exactly as before; only the DEFAULT
264
+ * moved. */
265
+ export function toolsDir(opts: RepoRootOptions = {}): string {
266
+ return join(repoRoot(opts), ".c64-re-tools");
267
+ }
268
+
185
269
  /** The one shared directory name every module in this skill reads/writes
186
- * host-synchronised state through -- `join(repoRoot(...), ".vice-supervisor")`,
187
- * so the literal directory name also has exactly one definition. */
270
+ * host-synchronised state through -- `join(toolsDir(...), "supervisor")`,
271
+ * a subdirectory of the single tool-written root `toolsDir()` owns. */
188
272
  export function supervisorDir(opts: RepoRootOptions = {}): string {
189
- return join(repoRoot(opts), ".vice-supervisor");
273
+ return join(toolsDir(opts), "supervisor");
190
274
  }
191
275
 
192
276
  // Fires once per process, on whatever entry point happens to import THIS