@henols/vice-mcp 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +2 -1
  2. package/THIRD-PARTY-NOTICES.md +1 -1
  3. package/anno-acme-ident.ts +97 -0
  4. package/anno-cli.ts +1465 -0
  5. package/anno-confidence.ts +233 -0
  6. package/anno-coverage.ts +2465 -0
  7. package/anno-d64.ts +310 -0
  8. package/anno-derive.ts +590 -0
  9. package/anno-details.ts +169 -0
  10. package/anno-enum-gen.ts +533 -0
  11. package/anno-export-asm.ts +1310 -0
  12. package/anno-index.ts +150 -0
  13. package/anno-memmap-render.ts +672 -0
  14. package/anno-regbits-gen.ts +421 -0
  15. package/anno-regbits.json +1370 -0
  16. package/anno-register.ts +240 -0
  17. package/anno-store.ts +3486 -0
  18. package/anno-symbols.ts +266 -0
  19. package/anno-tools.ts +2111 -0
  20. package/anno-types.ts +1636 -0
  21. package/block-class.ts +201 -0
  22. package/build.ts +1 -1
  23. package/capability-registry.ts +3 -1
  24. package/disasm-decoder.ts +14 -14
  25. package/disasm-opcodes.ts +4 -4
  26. package/disasm-renderer.ts +2 -2
  27. package/hostpath.ts +1 -1
  28. package/install-resources.ts +1 -1
  29. package/package.json +23 -3
  30. package/prg-image.ts +119 -0
  31. package/repo-root.ts +20 -5
  32. package/resources/broker-launch.mjs +8 -4
  33. package/resources/vice-launcher.sh +3 -3
  34. package/stock-address.ts +5 -5
  35. package/stock-cia.ts +2 -2
  36. package/stock-condition.ts +7 -7
  37. package/stock-connect.ts +1 -1
  38. package/stock-dispatch.ts +35 -5
  39. package/stock-execution.ts +5 -3
  40. package/stock-input.ts +9 -9
  41. package/stock-machine.ts +17 -6
  42. package/stock-protocol.ts +16 -11
  43. package/stock-registers.ts +54 -29
  44. package/stock-sprites.ts +3 -3
  45. package/stock-symbols.ts +33 -9
  46. package/stock-timing.ts +1 -1
  47. package/stock-vicii.ts +1 -1
  48. package/version.ts +1 -1
  49. package/vice-proxy.ts +168 -0
package/stock-cia.ts CHANGED
@@ -23,7 +23,7 @@
23
23
  // field naming the sharing address.
24
24
  //
25
25
  // Bit-field names below were transcribed ONCE from
26
- // `.claude/skills/c64-memory-mapping/memmap.json`'s entries for $DC00,
26
+ // `src/skills/c64-memory-mapping/memmap.json`'s entries for $DC00,
27
27
  // $DC01, $DC02, $DC03, $DC08-$DC0F, $DD00, $DD01 and $DD0D, and
28
28
  // cross-checked at write time -- the same "committed literal, cross-checked
29
29
  // once, no automated drift check" posture Phase 4's D-06 already accepted
@@ -36,7 +36,7 @@
36
36
  // - This is NOT a keyboard-matrix read. $DC00/$DC01 expose only the
37
37
  // current column selection and row result; the full matrix is
38
38
  // `vice_keyboard_matrix`, which is provably unrecoverable on stock
39
- // (`docs/stock-vice-parity.md` SS A item 2) and is Phase 8's business.
39
+ // (`docs/stock-vice-parity.md` SS A item 2) -- the wire protocol has no matrix command, and `KEYBOARD_FEED` (0x72) injects buffer text only.
40
40
  // - WR-02 (2026-08-17): the port A/B joystick bits share their PINS with
41
41
  // the keyboard matrix's column-select ($DC00) and row-read ($DC01), and
42
42
  // a stock read halts the machine at an arbitrary PC -- often inside the
@@ -35,10 +35,10 @@
35
35
  // last gate before the wire and re-validates every literal and every
36
36
  // kind itself, even though parseConditionString() and conditionFromJson()
37
37
  // also validate on the way in.
38
- // - Phase 6's GAIN-06 extends this AST with raster semantics (finer-grained
39
- // raster/cycle conditions) rather than replacing it or adding a second,
40
- // parallel condition-building path. Any future raster work grows this
41
- // module's types, it does not fork them.
38
+ // - GAIN-06's raster-semantics extension (finer-grained raster/cycle
39
+ // conditions) was cut with the whole of Stock-Only Gains, 2026-08-17
40
+ // (see docs/stock-vice-parity.md's dated cut record). Widening this AST
41
+ // is unclaimed scope with no owner -- grow these types, never fork them.
42
42
  //
43
43
  // This module has no handlers and no dispatch entries -- a later plan
44
44
  // consumes emitCondition()'s output as the only thing a condition-set
@@ -545,9 +545,9 @@ function parseSingleComparison(text: string, originalExpr: string): ConditionNod
545
545
  * conditionFromJson() produces and returns it; it never emits text itself,
546
546
  * so emitCondition() remains the only producer of wire text.
547
547
  *
548
- * Accepted input, deliberately narrow (widening this grammar is Phase 6's
549
- * GAIN-06 decision, not a maintenance liberty -- do not implement a general
550
- * expression parser):
548
+ * Accepted input, deliberately narrow (GAIN-06's plan to widen this grammar
549
+ * was cut along with Stock-Only Gains, 2026-08-17; widening it is unclaimed
550
+ * scope with no owner -- do not implement a general expression parser):
551
551
  * - a single comparison, with or without surrounding parentheses:
552
552
  * "A == $42", "(PC == $c000)", "SP <= $ff", "RL == $64"
553
553
  * - a conjunction/disjunction where EVERY comparison is individually
package/stock-connect.ts CHANGED
@@ -534,7 +534,7 @@ export async function stockReconnect(session: StockConnectSession, { lastToolCal
534
534
 
535
535
  if (baselineEpoch === null || currentEpoch === null || currentEpoch !== baselineEpoch) {
536
536
  throw new MachineRestartedError(
537
- `stockConnect: reconnect to target ${session.targetId} could not prove machine identity across the reconnect ` +
537
+ `stockReconnect: reconnect to target ${session.targetId} could not prove machine identity across the reconnect ` +
538
538
  `(baseline epoch ${String(baselineEpoch)}, current epoch ${String(currentEpoch)})`,
539
539
  { baselineEpoch, currentEpoch, where: "stock-connect.ts:stockReconnect", lastToolCall },
540
540
  );
package/stock-dispatch.ts CHANGED
@@ -28,6 +28,7 @@ import { resolve, join } from "node:path";
28
28
 
29
29
  import type { ViceBackend } from "./backend-detect.mts";
30
30
  import type { ToolInfo } from "./vice.ts";
31
+ import { capabilityRefusalMessage } from "./capability-registry.ts";
31
32
  import { type HeldLease } from "./vice-broker-client.ts";
32
33
  import { stockConnect, stockDisconnect, stockReconnect, type StockConnectSession, type StockConnectDeps } from "./stock-connect.ts";
33
34
  import {
@@ -591,7 +592,22 @@ export function withDerivedTool(
591
592
  * comment on why). Built through stockAnswer() so the answer now also
592
593
  * carries `runState` (D-06: every stock answer, and `vice_ping` is a stock
593
594
  * answer) alongside every field that was already there.
595
+ *
596
+ * 2026-08-19 finding (closed Phase 15 plan 15-09): `resolvedBinaryPath` is a
597
+ * ONE-TIME, MCP-server-process-startup `$PATH` probe (see vice-proxy.ts's
598
+ * `ACTIVE_BACKEND` comment) -- it is independent of which binary the broker
599
+ * actually leased for THIS request. `resolvedBinaryPathScope` below is an
600
+ * additive, backward-compatible sibling field (the existing field name and
601
+ * shape are unchanged) that carries that qualification into the answer
602
+ * itself, so a caller reading the response -- not just this source comment --
603
+ * learns not to treat the path as this request's authoritative binary
604
+ * identity.
594
605
  */
606
+ const RESOLVED_BINARY_PATH_SCOPE =
607
+ "one-time MCP-server-process-startup PATH probe; NOT the binary the broker leased for this " +
608
+ "request -- for the authoritative per-instance binary, read the broker's own launch record " +
609
+ "(epoch.json's vice_bin field)";
610
+
595
611
  const handlePing: StockSessionHandler = async (_args, session, deps) => {
596
612
  return stockAnswer(session.client, {
597
613
  status: "ok",
@@ -602,6 +618,9 @@ const handlePing: StockSessionHandler = async (_args, session, deps) => {
602
618
  // it, an agent reading `"x64sc"` cannot tell "this is where the binary is"
603
619
  // from "this is what we were told to look for, and we could not find it".
604
620
  resolvedBinaryPathIsResolved: deps.resolvedBinaryPathIsResolved ?? false,
621
+ // 2026-08-19 finding: names what resolvedBinaryPath actually is (a
622
+ // startup-time probe) and where to look instead for a per-request answer.
623
+ resolvedBinaryPathScope: RESOLVED_BINARY_PATH_SCOPE,
605
624
  capabilities: session.capabilities,
606
625
  });
607
626
  };
@@ -611,11 +630,11 @@ const handlePing: StockSessionHandler = async (_args, session, deps) => {
611
630
  * not an oversight (Task 2, plan 03-12):
612
631
  * - `vice_checkpoint_set_ignore_count` (D-15)
613
632
  * - `vice_snapshot_list` (D-16 -- deleted from both manifests)
614
- * - `vice_disk_detach` (D-13 -- Phase 7, via the text monitor)
615
- * - `vice_joystick_tap` (needs a resume plus Phase 7's timing route)
633
+ * - `vice_disk_detach` (CUT from scope 2026-08-17 -- docs/stock-vice-parity.md's dated cut record)
634
+ * - `vice_joystick_tap` (permanently excluded -- stock-input.ts's own header)
616
635
  * - `vice_disk_read_sector` (CUT from scope 2026-08-17 -- no skill calls it; see ROADMAP.md "Cut from scope (v0.2.0, 2026-08-17)" and docs/stock-vice-parity.md item 6)
617
636
  * - `vice_sid_get_state` and the low-level keyboard family (hard losses)
618
- * - `vice_machine_config_get` / `vice_machine_config_set` (Phase 6)
637
+ * - `vice_machine_config_get` / `vice_machine_config_set` (CUT from scope 2026-08-17 along with the whole of Stock-Only Gains -- docs/stock-vice-parity.md's dated cut record)
619
638
  * `dispatchStock()`'s miss branch already refuses any of these by name,
620
639
  * without reading `deps` -- there is nothing else to add for them here.
621
640
  */
@@ -732,9 +751,20 @@ export function stockHandlerFor(name: string): StockHandler | undefined {
732
751
  export async function dispatchStock(name: string, args: Record<string, unknown>, deps: StockDispatchDeps): Promise<StockToolResult> {
733
752
  const handler = stockHandlerFor(name);
734
753
  if (!handler) {
754
+ // WR-13: route through capability-registry.ts's ONE authoritative
755
+ // refusal renderer rather than a second, locally-composed wording --
756
+ // that renderer knows which backend ACTUALLY provides each name
757
+ // (avoiding the false "the fork backend provides this tool" claim for a
758
+ // stock-only-gain name) and never uses "wait for a later phase" framing
759
+ // for a hardware loss. Fall back to an internal-inconsistency message
760
+ // ONLY when the registry has no entry at all for `name` -- meaning the
761
+ // tool is advertised on the stock manifest (so it reached this branch)
762
+ // but stockHandlerFor() has no dispatch entry AND capability-registry.ts
763
+ // has no divergence entry either: a bug to report, not a capability gap.
735
764
  return isErrorText(
736
- `${name} is not implemented by the stock backend -- the fork backend provides this tool. ` +
737
- `Set VICE_BACKEND=fork to use it there, or wait for a later phase to extend the stock dispatch table.`,
765
+ capabilityRefusalMessage(name, "stock") ??
766
+ `${name} is advertised on the stock backend's manifest but has no handler in the stock ` +
767
+ `dispatch table -- this is an internal inconsistency, not a capability gap; please file an issue.`,
738
768
  );
739
769
  }
740
770
  return handler(args, deps);
@@ -224,9 +224,11 @@ export const handleExecutionRun: StockSessionHandler = async (args, session) =>
224
224
  * the derived run state is "unknown" (D-07, via refuseIfUnknown()).
225
225
  *
226
226
  * `stepOver: true`'s runtime semantic (skip a JSR's subroutine as one step)
227
- * is [ASSUMED] -- RESEARCH.md Assumptions Log row A2 -- never probed against
228
- * a real JSR. See `.planning/todos/pending/2026-08-14-probe-phase3-assumed-wire-details.md`
229
- * for the outstanding probe debt. This is NOT claimed as verified here.
227
+ * was live-probed against a real JSR on fork VICE 3.10 and CONFIRMED
228
+ * (RESEARCH.md Assumptions Log row A2, closed by 13-PROBE-RESULTS.md §A2):
229
+ * the post-step PC landed at JSR+3, matching a full subroutine skip, and
230
+ * the result was reproduced identically across two independent live
231
+ * sessions.
230
232
  */
231
233
  export const handleExecutionStep: StockSessionHandler = async (args, session) => {
232
234
  const unexpected = refuseUnexpectedArgs(args, ["count", "stepOver"], "vice_execution_step");
package/stock-input.ts CHANGED
@@ -25,10 +25,10 @@
25
25
  // resumes explicitly, on its own schedule.
26
26
  // - Never add vice_joystick_tap. A tap needs the machine to RUN for a
27
27
  // measured interval -- an unrequested EXIT (forbidden by D-05) plus a
28
- // frame/cycle measurement that does not exist on stock until Phase 7's
29
- // timing route lands (docs/stock-vice-parity.md section A item 7).
30
- // vice_joystick_set (hold/release/centre) satisfies DIRECT-07's
31
- // joystick half in the meantime.
28
+ // frame/cycle measurement stock's wire protocol has no route for at
29
+ // all (docs/stock-vice-parity.md section A item 7). This exclusion is
30
+ // permanent, not pending: vice_joystick_set (hold/release/centre)
31
+ // satisfies DIRECT-07's joystick half instead.
32
32
  // - Never construct an ok-answer outside stockAnswer(). Every successful
33
33
  // result below is built through it, never a bare
34
34
  // `{ content: [...], isError: false }` literal.
@@ -187,11 +187,11 @@ function isValidDirection(value: string): value is JoystickDirection {
187
187
  * the fork's exact names and documented string-or-array shape.
188
188
  *
189
189
  * vice_joystick_tap is deliberately absent from this module (and the whole
190
- * stock manifest) -- a tap needs the machine to run for a measured
191
- * interval, which is an unrequested EXIT (forbidden by D-05) plus a
192
- * frame/cycle measurement stock does not have until Phase 7's timing route
193
- * lands (docs/stock-vice-parity.md section A item 7). Do not approximate it
194
- * with a sleep; do not implement it here.
190
+ * stock manifest), permanently -- a tap needs the machine to run for a
191
+ * measured interval, which is an unrequested EXIT (forbidden by D-05) plus
192
+ * a frame/cycle measurement stock's wire protocol has no route for at all
193
+ * (docs/stock-vice-parity.md section A item 7). Do not approximate it with
194
+ * a sleep; do not implement it here.
195
195
  */
196
196
  export const handleJoystickSet: StockSessionHandler = async (args, session) => {
197
197
  if (!isPlainObject(args)) {
package/stock-machine.ts CHANGED
@@ -12,17 +12,17 @@
12
12
  // WHAT NOT TO DO:
13
13
  // - Never gate or deny vice_machine_reset's hard mode. CLAUDE.md's
14
14
  // power-cycle warning is about RESOURCE_SET (0x52) writes to
15
- // MachineVideoStandard/VICIIModel/MachinePowerFrequency -- Phase 6
16
- // territory, a DIFFERENT opcode entirely. RESET (0xcc) is a distinct
15
+ // MachineVideoStandard/VICIIModel/MachinePowerFrequency -- the CUT
16
+ // vice_machine_config_get/set pair's resources (docs/stock-vice-parity.md), a DIFFERENT opcode entirely. RESET (0xcc) is a distinct
17
17
  // command, and an agent-requested hard reset via RESET is exactly what
18
18
  // DIRECT-06 asks for. It needs no deny-list (RESEARCH.md Pitfall 1).
19
19
  // - Never look for a per-unit disk-attach route mid-implementation.
20
20
  // AUTOSTART (0xdd) has NO drive-unit field on the wire at all -- this is
21
21
  // a protocol gap, not a code bug you can fix by looking harder
22
22
  // (RESEARCH.md Pitfall 2).
23
- // - Never add a disk-detach handler here. D-13 ships that tool in Phase 7
24
- // through the text monitor -- grep-gated to zero occurrences of its name
25
- // in this file's own acceptance criteria.
23
+ // - Never add a disk-detach handler here. D-13's vice_disk_detach was
24
+ // CUT from scope 2026-08-17 (docs/stock-vice-parity.md) -- grep-gated
25
+ // to zero occurrences of its name in this file's own acceptance criteria.
26
26
  // - Never build a host path outside stock-paths.ts. Every filename this
27
27
  // file sends through the wire goes through that same one wrapper --
28
28
  // grep-gated to zero direct hostPath()/hostPathCandidates() calls here.
@@ -150,7 +150,18 @@ export const handleAutostart: StockSessionHandler = async (args, session) => {
150
150
  * at all, so an agent told "attached to unit 9" when the image landed on
151
151
  * unit 8 would debug the wrong drive. See docs/stock-vice-parity.md's D-14
152
152
  * entry.
153
+ *
154
+ * The returned `approximation` string names BOTH real side effects Phase 13
155
+ * plan 13-03's live A5 probe observed against real fork VICE 3.10: a full
156
+ * machine reset AND a program load, not "attach without disturbing machine
157
+ * state." Exported so the pinning test derives its expectation from this
158
+ * constant rather than re-typing the sentence, so the two cannot drift. See
159
+ * `.planning/phases/13-external-verification/13-PROBE-RESULTS.md` § A5.
153
160
  */
161
+ export const DISK_ATTACH_APPROXIMATION =
162
+ "AUTOSTART (D-14): performs a full machine reset and loads a program from the image; " +
163
+ "unlike vice_autostart it does not issue a final run step, as far as observed.";
164
+
154
165
  export const handleDiskAttach: StockSessionHandler = async (args, session) => {
155
166
  const a = isPlainObject(args) ? args : {};
156
167
 
@@ -182,7 +193,7 @@ export const handleDiskAttach: StockSessionHandler = async (args, session) => {
182
193
  unit: 8,
183
194
  path: containerPath,
184
195
  sentPath,
185
- approximation: "AUTOSTART with the run flag clear (D-14)",
196
+ approximation: DISK_ATTACH_APPROXIMATION,
186
197
  });
187
198
  } catch (err) {
188
199
  return convertWireError("vice_disk_attach", err);
package/stock-protocol.ts CHANGED
@@ -720,12 +720,15 @@ export function registersSetBody({ memspace, items }: RegistersSetBodyOptions):
720
720
 
721
721
  // ---------------------------------------------------------------------------
722
722
  // Execution and machine-control body encoders (Phase 3, Task 2). Every
723
- // body layout below is [CITED] against the official VICE manual (§13) but
724
- // has NOT been exercised against a real binary in this environment -- each
725
- // JSDoc says so explicitly, and where RESEARCH.md's Assumptions Log flags a
726
- // behavioural (not wire-shape) assumption, the JSDoc names the row (A2, A3,
727
- // A5) and points at .planning/todos/pending/ for the probe debt. None of
728
- // these are claimed as verified.
723
+ // body layout below is [CITED] against the official VICE manual (§13);
724
+ // each encoder's own JSDoc states whether its behavioural (not wire-shape)
725
+ // claim has since been live-probed. RESEARCH.md Assumptions Log row A2 was
726
+ // closed CONFIRMED by 13-PROBE-RESULTS.md. Two rows below are still
727
+ // unconfirmed -- A3 stayed INCONCLUSIVE (no observable signal either way)
728
+ // and A5 was CONTRADICTED (still pending its correction) -- and each of
729
+ // those two encoders' own JSDoc, below, still carries its own [ASSUMED]
730
+ // naming that row and pointing at .planning/todos/pending/ for the
731
+ // remaining probe debt.
729
732
  // ---------------------------------------------------------------------------
730
733
 
731
734
  export interface AdvanceInstructionsBodyOptions {
@@ -739,9 +742,11 @@ export interface AdvanceInstructionsBodyOptions {
739
742
  * exercised, with stepOver=0 only, in probe-binmon.mjs's async-events check]
740
743
  *
741
744
  * `stepOver = true`'s runtime meaning (skip a `JSR`'s subroutine as one
742
- * step, matching the fork's own `stepOver` field name) is [ASSUMED] --
743
- * RESEARCH.md Assumptions Log row A2 -- never probed against a real `JSR`.
744
- * See `.planning/todos/pending/` for the outstanding probe debt.
745
+ * step, matching the fork's own `stepOver` field name) was live-probed
746
+ * against a real `JSR` on fork VICE 3.10 and CONFIRMED (RESEARCH.md
747
+ * Assumptions Log row A2, closed by 13-PROBE-RESULTS.md §A2): the post-step
748
+ * PC landed at JSR+3, reproduced identically across two independent live
749
+ * sessions.
745
750
  */
746
751
  export function advanceInstructionsBody({ stepOver = false, count = 1 }: AdvanceInstructionsBodyOptions = {}): Buffer {
747
752
  if (!Number.isInteger(count) || count < 1 || count > 0xffff) {
@@ -816,8 +821,8 @@ export interface ResetBodyOptions {
816
821
  * [CITED docs/phase0-binmon-findings.md §5]
817
822
  *
818
823
  * NOT the RESOURCE_SET (0x52) power-cycle hazard CLAUDE.md warns about
819
- * (`MachineVideoStandard`/`VICIIModel`/`MachinePowerFrequency`, Phase 6
820
- * territory) -- this is a distinct opcode, and an agent-requested hard
824
+ * (`MachineVideoStandard`/`VICIIModel`/`MachinePowerFrequency`, the CUT
825
+ * vice_machine_config_get/set pair's resources) -- a distinct opcode, and an agent-requested hard
821
826
  * reset via RESET is exactly what DIRECT-06 asks for. It needs no
822
827
  * deny-list. This is RESEARCH.md's Pitfall 1; this comment is what stops a
823
828
  * later reviewer from "fixing" it by adding one.
@@ -68,12 +68,19 @@ export interface RegisterCatalog {
68
68
  /** The one module-level catalog map, keyed on the session object itself --
69
69
  * NOT on session.client -- so a fresh stockReconnect() (which returns a
70
70
  * brand-new session) is indistinguishable from "never fetched" and simply
71
- * fetches again, with no manual invalidation path required anywhere. */
71
+ * fetches again, with no manual invalidation path required anywhere.
72
+ *
73
+ * Caches the in-flight PROMISE, not the resolved catalog (15-04, IN-02):
74
+ * two handlers racing on a fresh session both read `catalogs.get(session)`
75
+ * before either write lands, so caching only the resolved value let both
76
+ * send their own REGISTERS_AVAILABLE. Caching the promise means the second
77
+ * caller awaits the SAME in-flight request. A rejected promise is evicted
78
+ * (see registerCatalogFor()) so a failed fetch is retried, never memoised. */
72
79
  // Single-line by design: the ONLY line in this file naming the garbage-
73
80
  // collectable, session-keyed map primitive directly (grep-gated -- see
74
81
  // this plan's own acceptance criteria). Every other reference goes
75
82
  // through this factory, never a second construction call site.
76
- function freshCatalogMap(): WeakMap<StockConnectSession, RegisterCatalog> { return new WeakMap<StockConnectSession, RegisterCatalog>(); }
83
+ function freshCatalogMap(): WeakMap<StockConnectSession, Promise<RegisterCatalog>> { return new WeakMap<StockConnectSession, Promise<RegisterCatalog>>(); }
77
84
 
78
85
  let catalogs = freshCatalogMap();
79
86
 
@@ -86,15 +93,22 @@ export function resetRegisterCatalogsForTest(): void {
86
93
 
87
94
  /**
88
95
  * Resolves `session`'s register catalog, fetching it through
89
- * REGISTERS_AVAILABLE (0x83) exactly once and caching the result on the
90
- * session object. Every subsequent call for the SAME session object
91
- * returns the cached catalog with no further wire traffic.
96
+ * REGISTERS_AVAILABLE (0x83) exactly once and caching the IN-FLIGHT
97
+ * PROMISE on the session object -- not just the resolved value, so two
98
+ * concurrent callers on a fresh session (before either fetch has
99
+ * resolved) share the SAME REGISTERS_AVAILABLE round trip rather than
100
+ * each sending their own (15-04, IN-02). Every subsequent call for the
101
+ * SAME session object returns the same promise (and, once it settles,
102
+ * the same resolved catalog) with no further wire traffic.
92
103
  *
93
104
  * Refuses (throws a plain Error, converted by the caller through
94
105
  * convertWireError()) an empty enumeration rather than caching it: a
95
106
  * build that enumerates zero registers cannot support
96
107
  * vice_registers_set, and that failure must be visible on every call,
97
- * never silently cached as "zero registers, nothing to resolve".
108
+ * never silently cached as "zero registers, nothing to resolve". A
109
+ * rejected fetch, whether from an empty enumeration or a wire error, is
110
+ * evicted from the cache before this function returns, so the NEXT call retries instead
111
+ * of permanently memoising the failure.
98
112
  */
99
113
  export async function registerCatalogFor(session: StockConnectSession): Promise<RegisterCatalog> {
100
114
  const existing = catalogs.get(session);
@@ -102,30 +116,41 @@ export async function registerCatalogFor(session: StockConnectSession): Promise<
102
116
  return existing;
103
117
  }
104
118
 
105
- const response = await session.client.send(CommandType.RegistersAvailable, memspaceBody({ memspace: 0x00 }));
106
- if (response.type !== "registers_available") {
107
- throw new Error(`registerCatalogFor: expected a registers_available reply, got "${response.type}"`);
108
- }
109
- if (response.registers.length === 0) {
110
- throw new Error(
111
- "registerCatalogFor: the connected VICE build enumerated zero registers via REGISTERS_AVAILABLE -- " +
112
- "it cannot support vice_registers_set, and this failure must be named rather than cached as an empty catalog",
113
- );
114
- }
119
+ const pending = (async (): Promise<RegisterCatalog> => {
120
+ const response = await session.client.send(CommandType.RegistersAvailable, memspaceBody({ memspace: 0x00 }));
121
+ if (response.type !== "registers_available") {
122
+ throw new Error(`registerCatalogFor: expected a registers_available reply, got "${response.type}"`);
123
+ }
124
+ if (response.registers.length === 0) {
125
+ throw new Error(
126
+ "registerCatalogFor: the connected VICE build enumerated zero registers via REGISTERS_AVAILABLE -- " +
127
+ "it cannot support vice_registers_set, and this failure must be named rather than cached as an empty catalog",
128
+ );
129
+ }
115
130
 
116
- const byName = new Map<string, { id: number; sizeBits: number; name: string }>();
117
- const byId = new Map<number, { sizeBits: number; name: string }>();
118
- for (const reg of response.registers) {
119
- // reg.size is stock-protocol.ts's own field name for the wire's size
120
- // byte (its parser is unchanged by this plan); this module renames it
121
- // to sizeBits at the point it enters the catalog so every downstream
122
- // reader sees the unit named in the type.
123
- byName.set(reg.name.toUpperCase(), { id: reg.id, sizeBits: reg.size, name: reg.name });
124
- byId.set(reg.id, { sizeBits: reg.size, name: reg.name });
125
- }
126
- const catalog: RegisterCatalog = { byName, byId };
127
- catalogs.set(session, catalog);
128
- return catalog;
131
+ const byName = new Map<string, { id: number; sizeBits: number; name: string }>();
132
+ const byId = new Map<number, { sizeBits: number; name: string }>();
133
+ for (const reg of response.registers) {
134
+ // reg.size is stock-protocol.ts's own field name for the wire's size
135
+ // byte (its parser is unchanged by this plan); this module renames it
136
+ // to sizeBits at the point it enters the catalog so every downstream
137
+ // reader sees the unit named in the type.
138
+ byName.set(reg.name.toUpperCase(), { id: reg.id, sizeBits: reg.size, name: reg.name });
139
+ byId.set(reg.id, { sizeBits: reg.size, name: reg.name });
140
+ }
141
+ return { byName, byId };
142
+ })();
143
+
144
+ catalogs.set(session, pending);
145
+ // Evict on rejection so a failed fetch is retried by the next call
146
+ // rather than memoised forever -- only if no newer promise has already
147
+ // replaced this one for the same session.
148
+ pending.catch(() => {
149
+ if (catalogs.get(session) === pending) {
150
+ catalogs.delete(session);
151
+ }
152
+ });
153
+ return pending;
129
154
  }
130
155
 
131
156
  // ---------------------------------------------------------------------------
package/stock-sprites.ts CHANGED
@@ -12,14 +12,14 @@
12
12
  // PROVENANCE (required reading before touching the four geometry
13
13
  // functions below): vicBank(), vicBankBase(), screenBase() and
14
14
  // spriteDataAddress() are PORTED, NOT RE-DERIVED, from
15
- // .claude/skills/c64-ram-capture/scripts/dump-artifacts.mjs's own
15
+ // src/skills/c64-ram-capture/scripts/dump-artifacts.mjs's own
16
16
  // vicBank()/screenBase()/spriteDataAddresses map, which carries a
17
17
  // committed, verified fixture: dd00_raw=193 (0xC1), d018_raw=49 (0x31) ->
18
18
  // screen_base=35840. stock-sprites.test.ts re-asserts the SAME fixture as
19
19
  // its own cross-check -- do not change any of the four expressions without
20
20
  // also updating that committed fixture's provenance. The skill's
21
- // JavaScript is copied here, never imported at runtime -- .claude/skills/
22
- // is a different package, absent from .claude/mcp/vice's files[], so a
21
+ // JavaScript is copied here, never imported at runtime -- src/skills/
22
+ // is a different package, absent from src/mcp/vice's files[], so a
23
23
  // runtime cross-package import would be missing from the published
24
24
  // tarball.
25
25
  //
package/stock-symbols.ts CHANGED
@@ -26,11 +26,17 @@
26
26
  // The confirmed input format is a VICE label file, one `al C:xxxx .Name`
27
27
  // line per symbol, verified against ACME's `--vicelabels` output via
28
28
  // acme-build/scripts/acme.mjs's own parser (curateLabels(),
29
- // `/^al\s+C:[0-9a-f]+\s+\.(\S+)/i`). STATED ASSUMPTION, NOT A VERIFIED FACT:
30
- // regenerator2000's `--export_lbl` is *expected* to emit the same syntax,
31
- // but R2000-16(c) has never been run -- hence the parser below SKIPS
32
- // unrecognised lines rather than refusing the whole file, and no comment or
33
- // doc here may claim "regenerator2000-compatible" as verified.
29
+ // `/^al\s+C:[0-9a-f]+\s+\.(\S+)/i`). VERIFIED (Phase 9, ANNO-16(c)):
30
+ // the external analyser 0.9.20's `--export_lbl` was run against the
31
+ // probe-illegal.prg-derived fixture and emitted `al C:0810 .init_screen`,
32
+ // which matches this module's own VICE_LABEL_LINE_RE
33
+ // (`/^al\s+C:([0-9a-fA-F]{1,4})\s+\.(\S+)/`) exactly. This claim is SCOPED to
34
+ // the external analyser 0.9.20 and that fixture -- not to all inputs forever (the
35
+ // same scoping caveat ROADMAP.md applies to Phase 9's criterion 3(3) `pass`).
36
+ // The parser below still SKIPS unrecognised lines rather than refusing the
37
+ // whole file: a future analyser version, a hand-edited label file, or
38
+ // a different exporter entirely can still produce lines this format should
39
+ // tolerate rather than reject outright.
34
40
  //
35
41
  // WHAT NOT TO DO:
36
42
  // - Never add a second resolver holder or call setSymbolResolver() from
@@ -75,8 +81,12 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
75
81
  const VICE_LABEL_LINE_RE = /^al\s+C:([0-9a-fA-F]{1,4})\s+\.(\S+)/;
76
82
 
77
83
  /** T-05-02-03: three independent resource ceilings, each refusing with both
78
- * the observed value and the limit named. */
79
- const MAX_LABEL_FILE_BYTES = 2 * 1024 * 1024;
84
+ * the observed value and the limit named. `MAX_LABEL_FILE_BYTES` is exported
85
+ * (11-08, Rule A20) so `anno-symbols.ts`'s `exportLabels()`/`importLabels()`
86
+ * can apply the SAME byte ceiling to an external-analyser-produced/-consumed
87
+ * `.lbl` file before ever calling `parseViceLabelFile()` below -- never a
88
+ * second hand-copied number. */
89
+ export const MAX_LABEL_FILE_BYTES = 2 * 1024 * 1024;
80
90
  const MAX_LABEL_FILE_LINES = 50000;
81
91
  const MAX_SYMBOLS = 20000;
82
92
 
@@ -98,7 +108,11 @@ export class StockSymbolsError extends ViceError {
98
108
  }
99
109
  }
100
110
 
101
- interface SymbolTable {
111
+ /** Exported alongside `parseViceLabelFile()` (11-08, Rule A20) purely so a
112
+ * cross-module caller can name this shape in its own type annotations --
113
+ * this module's own internal state (`loadedTable` below) still never leaves
114
+ * this file. */
115
+ export interface SymbolTable {
102
116
  byName: Map<string, number>;
103
117
  byAddress: Map<number, string>;
104
118
  }
@@ -186,7 +200,17 @@ function resolveLabelFilePath(pathArg: unknown): string {
186
200
  // counted, never a whole-file refusal.
187
201
  // ---------------------------------------------------------------------------
188
202
 
189
- function parseViceLabelFile(text: string): {
203
+ /**
204
+ * Exported (11-08, Rule A20) so `anno-symbols.ts` can validate a
205
+ * external-analyser-produced `.lbl` file (or check a caller-supplied one
206
+ * BEFORE it is ever handed to a spawned analyser child) through THIS
207
+ * parser -- the ONE `al C:xxxx .Name` reader in this repo -- rather than
208
+ * adding a second copy of `VICE_LABEL_LINE_RE`. Ceiling violations
209
+ * (`MAX_LABEL_FILE_LINES`/`MAX_SYMBOLS`) throw `StockSymbolsError` exactly as
210
+ * they do for `handleSymbolsLoad` below; a caller across the module boundary
211
+ * is expected to surface that error verbatim, never re-wrap it.
212
+ */
213
+ export function parseViceLabelFile(text: string): {
190
214
  table: SymbolTable;
191
215
  symbolCount: number;
192
216
  skippedLines: number;
package/stock-timing.ts CHANGED
@@ -21,7 +21,7 @@
21
21
  // - Never assign `cycles: 0` or `cycles: null` for an unmeasurable
22
22
  // bracket. `measurable: false` with a `reason` and NO `cycles` key at
23
23
  // all is the only honest shape -- see the incident this rule exists to
24
- // prevent: `.claude/skills/c64-program-recon/references/observation-hazards.md`'s
24
+ // prevent: `src/skills/c64-program-recon/references/observation-hazards.md`'s
25
25
  // record of the fork's stopwatch reading 258,504,308 cycles and being
26
26
  // trusted as fact.
27
27
  // - Never hardcode a register id for LIN/CYC/PC. `registerCatalogFor()`
package/stock-vicii.ts CHANGED
@@ -61,7 +61,7 @@
61
61
  //
62
62
  // FIELD-NAME PROVENANCE (Assumption A4's mitigation): every bit-field name
63
63
  // below was transcribed once from
64
- // .claude/skills/c64-memory-mapping/memmap.json's own entries for $D011
64
+ // src/skills/c64-memory-mapping/memmap.json's own entries for $D011
65
65
  // (Screen control register #1 / VIC Control Register), $D012 (raster line),
66
66
  // $D016 (Screen control register #2 / VIC Control Register), $D018 (Memory
67
67
  // setup register / VIC Memory Control Register), $D019 (Interrupt status
package/version.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // The ONE authoritative implementation of this project's version-resolution
2
2
  // algorithm (D-5). Before this file existed the repo carried FOUR
3
3
  // hand-maintained version strings and none of them were true:
4
- // `.claude/mcp/vice/package.json` and `installer/package.json` were stale
4
+ // `src/mcp/vice/package.json` and `installer/package.json` were stale
5
5
  // placeholders CI never touched between releases, `.claude-plugin/plugin.json`
6
6
  // was bumped by NO automation at all, and `vice-proxy.ts`'s own
7
7
  // `PROXY_VERSION` literal -- advertised to every MCP client over