@maka/maka-cli 5.90.0 → 5.92.0

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 (115) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +9 -9
  3. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -1
  4. package/bundle/typescript/src/commands/game/sideQuest/augmentations.d.ts +54 -3
  5. package/bundle/typescript/src/commands/game/sideQuest/augmentations.js +987 -53
  6. package/bundle/typescript/src/commands/game/sideQuest/augmentations.js.map +1 -1
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.d.ts +0 -14
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +23 -1
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js.map +1 -1
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +1 -1
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js.map +1 -1
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/breach.d.ts +10 -2
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/breach.js +11 -2
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/breach.js.map +1 -1
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js +14 -1
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js.map +1 -1
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +30 -3
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js.map +1 -1
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +5 -1
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js +5 -1
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js.map +1 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +10 -1
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +2 -2
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js +2 -2
  28. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js.map +1 -1
  29. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js +8 -1
  30. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js.map +1 -1
  31. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js +3 -0
  32. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js.map +1 -1
  33. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +2 -2
  34. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js.map +1 -1
  35. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +55 -17
  36. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js.map +1 -1
  37. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js +1 -1
  38. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js.map +1 -1
  39. package/bundle/typescript/src/commands/game/sideQuest/commands/project.js +12 -0
  40. package/bundle/typescript/src/commands/game/sideQuest/commands/project.js.map +1 -1
  41. package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js +1 -1
  42. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +14 -4
  43. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -1
  44. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js +3 -3
  45. package/bundle/typescript/src/commands/game/sideQuest/engine-version.d.ts +1 -1
  46. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +109 -1
  47. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js.map +1 -1
  48. package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.d.ts +1 -1
  49. package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.js +62 -16
  50. package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.js.map +1 -1
  51. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +26 -3
  52. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
  53. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +95 -3
  54. package/bundle/typescript/src/commands/game/sideQuest/game.js +237 -15
  55. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  56. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js +5 -1
  57. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js.map +1 -1
  58. package/bundle/typescript/src/commands/game/sideQuest/models/inventory.d.ts +13 -0
  59. package/bundle/typescript/src/commands/game/sideQuest/models/inventory.js +45 -2
  60. package/bundle/typescript/src/commands/game/sideQuest/models/inventory.js.map +1 -1
  61. package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +65 -1
  62. package/bundle/typescript/src/commands/game/sideQuest/models/item.js +83 -1
  63. package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
  64. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +153 -3
  65. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +197 -12
  66. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  67. package/bundle/typescript/src/commands/game/sideQuest/models/scene.d.ts +73 -0
  68. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +97 -3
  69. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
  70. package/bundle/typescript/src/commands/game/sideQuest/qualities.d.ts +33 -1
  71. package/bundle/typescript/src/commands/game/sideQuest/qualities.js +80 -2
  72. package/bundle/typescript/src/commands/game/sideQuest/qualities.js.map +1 -1
  73. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene1.json +1 -1
  74. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +4 -4
  75. package/bundle/typescript/src/commands/game/sideQuest/skill-groups.d.ts +120 -0
  76. package/bundle/typescript/src/commands/game/sideQuest/skill-groups.js +135 -0
  77. package/bundle/typescript/src/commands/game/sideQuest/skill-groups.js.map +1 -0
  78. package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +34 -2
  79. package/bundle/typescript/src/commands/game/sideQuest/ui.js +36 -0
  80. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  81. package/bundle/typescript/src/commands/game/sideQuest/utilities/alarmed-staff.d.ts +116 -0
  82. package/bundle/typescript/src/commands/game/sideQuest/utilities/alarmed-staff.js +127 -0
  83. package/bundle/typescript/src/commands/game/sideQuest/utilities/alarmed-staff.js.map +1 -0
  84. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.d.ts +1 -1
  85. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js +123 -2
  86. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js.map +1 -1
  87. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.d.ts +131 -2
  88. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +170 -12
  89. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js.map +1 -1
  90. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.d.ts +139 -1
  91. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js +307 -5
  92. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js.map +1 -1
  93. package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.d.ts +41 -0
  94. package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.js +41 -0
  95. package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.js.map +1 -1
  96. package/bundle/typescript/src/commands/game/sideQuest/utilities/input-history.d.ts +98 -0
  97. package/bundle/typescript/src/commands/game/sideQuest/utilities/input-history.js +137 -0
  98. package/bundle/typescript/src/commands/game/sideQuest/utilities/input-history.js.map +1 -0
  99. package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.js +1 -0
  100. package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.js.map +1 -1
  101. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.d.ts +38 -0
  102. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +106 -0
  103. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
  104. package/bundle/typescript/src/commands/game/sideQuest/utilities/session-lock.js +19 -3
  105. package/bundle/typescript/src/commands/game/sideQuest/utilities/session-lock.js.map +1 -1
  106. package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js +1 -1
  107. package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js.map +1 -1
  108. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +2 -1
  109. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js.map +1 -1
  110. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.d.ts +22 -0
  111. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js +27 -0
  112. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js.map +1 -1
  113. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +79 -44
  114. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
  115. package/package.json +1 -1
@@ -18,11 +18,140 @@ import { ICatalogItem } from './catalog-data.js';
18
18
  * source.ts, so models can import it without pulling in a command.
19
19
  */
20
20
  export declare const ROLE_DOMAINS: Record<string, string[]>;
21
+ /** A catalog and the version it claims, from wherever it came. */
22
+ export interface ICatalogSource {
23
+ version: number;
24
+ items: ICatalogItem[];
25
+ origin: 'live' | 'cache' | 'seed';
26
+ }
27
+ /**
28
+ * A VERSION NUMBER, OR NOTHING.
29
+ *
30
+ * `Number(undefined)`, `Number('')` and `Number('1.26.0')` are all NaN,
31
+ * and every comparison against NaN is false -- so an unversioned payload
32
+ * silently loses to whatever it is compared with. That is the SAFE
33
+ * direction (the seed wins) but a silent one, so it is named here and
34
+ * logged at the call site rather than left to fall out of the operator.
35
+ */
36
+ export declare function parseCatalogVersion(raw: unknown): number | undefined;
37
+ /**
38
+ * Reads the cache file. Injectable into loadCatalog because the
39
+ * alternative is a test that reads the DEVELOPER'S home directory --
40
+ * and this repo has already lost an afternoon to a suite that went red
41
+ * because a real config file said `hints: false`. A test must not care
42
+ * what is on the machine running it.
43
+ */
44
+ type CacheReader = () => {
45
+ version: unknown;
46
+ items: ICatalogItem[];
47
+ } | undefined;
48
+ /**
49
+ * RECORD WHAT THE NETWORK RETURNED -- separated from the fetch so the
50
+ * wiring is reachable without a socket.
51
+ *
52
+ * This is not a test hatch, it is the step refreshCatalog was doing
53
+ * inline, and pulling it out is what makes the ACTUAL BUG SITE
54
+ * testable: the defect was never in how the candidates compare, it was
55
+ * that loadCatalog returned the live list without ever comparing it.
56
+ * A suite that only exercises chooseCatalog proves the comparison and
57
+ * says nothing about whether anyone calls it -- restoring the original
58
+ * one-line bug left such a suite completely green.
59
+ *
60
+ * An unusable version scores 0: present, comparable, and last.
61
+ */
62
+ export declare function applyLiveCatalog(items: ICatalogItem[], version: number | undefined): void;
21
63
  /** Fire-and-forget refresh from the site -- call once at session start;
22
64
  * the promise resolves when the live list (or a fallback) is settled. */
23
65
  export declare function refreshCatalog(): Promise<void>;
24
- /** The best catalog available right now: live > cached > baked-in. */
25
- export declare function loadCatalog(): ICatalogItem[];
66
+ /**
67
+ * The best catalog available right now: live > cached > baked-in --
68
+ * except that a cache OLDER than the shipped seed is not "best", it is
69
+ * stale, and the seed wins.
70
+ *
71
+ * THIS USED TO BE `cache?.items ?? CATALOG_ITEMS`, which handed an
72
+ * upgraded client its old catalog until a refresh happened to succeed.
73
+ * Found while wiring the drone hull lookup: this machine was serving a
74
+ * v6 cache of 198 items with 9 drones while the binary shipped a v10
75
+ * seed of 274 with 70 -- so three merged chunks were invisible offline.
76
+ *
77
+ * IT MATTERS MORE NOW THAN IT DID. When the catalog only fed shop
78
+ * listings, a stale cache meant "you cannot buy the new thing yet".
79
+ * maxDroneBoxes now reads Body from here, so staleness reaches COMBAT
80
+ * MATH: a drone the cache has never heard of falls through to the
81
+ * price-based fallback and is a different toughness than the same drone
82
+ * on a fresh client.
83
+ *
84
+ * A cache NEWER than the seed still wins, which is the whole point of
85
+ * the server being the source of truth.
86
+ *
87
+ * THE GUARD ORIGINALLY COVERED THE WRONG HALF, and the half it missed
88
+ * is the one nearly every player is on. It read:
89
+ *
90
+ * if (liveCatalog) return liveCatalog; // <-- unweighed
91
+ * ...then the version check, on the CACHE only
92
+ *
93
+ * so a FETCH SUCCEEDING was what defeated it. Measured 2026-08-31
94
+ * against the live endpoint: maka-cli.com served v6/198 rows while the
95
+ * shipped seed was v10/274. Online players got the 198 and offline
96
+ * players got the 274 -- being reachable was a DOWNGRADE, and the
97
+ * comment above about combat math was describing a live bug rather than
98
+ * a hypothetical: v6 carries 9 drones against v10's 70, so 61 drones
99
+ * missed catalogHullFor and took the price-based fallback for their
100
+ * condition monitor. Two clients, same drone, different toughness.
101
+ *
102
+ * The fix is to stop trusting ANY source's list because of where it
103
+ * came from, and weigh all three by the only thing that orders them.
104
+ */
105
+ export declare function loadCatalog(readCacheFn?: CacheReader): ICatalogItem[];
106
+ /**
107
+ * WHICH OF THE THREE WINS -- pure, and separated out because this is
108
+ * where the bug lived. A decision buried in early returns is one nobody
109
+ * can put a fixture in front of; a named function taking all three
110
+ * candidates can be asked every question directly.
111
+ *
112
+ * Highest version wins. On a TIE the fresher origin wins (live over
113
+ * cache over seed), because equal versions mean equal content and the
114
+ * live list is the one that just came from the source of truth.
115
+ *
116
+ * The seed is not a fallback here, it is a CANDIDATE -- it is the one
117
+ * list whose version is knowable with certainty, since it shipped in
118
+ * the same binary as this function.
119
+ */
120
+ export declare function chooseCatalog(live?: ICatalogSource, cache?: ICatalogSource): ICatalogSource;
121
+ /** Drop every memo -- the resolved winner and the hull index with it.
122
+ * Exported for tests; production invalidates via refreshCatalog. */
123
+ export declare function resetCatalogForTest(): void;
124
+ /**
125
+ * A DRONE'S HULL, from the catalog's own stat line.
126
+ *
127
+ * The engine used to size a drone's condition monitor off its RATING --
128
+ * which the catalog assigns by PRICE -- so durability tracked cost.
129
+ * Canon sizes it off BODY (SR5 p.199; anthroforms get two more,
130
+ * Rigger 5.0 p.145), and Body lives only in the `stats` string. This
131
+ * reads it back out.
132
+ *
133
+ * STRICT MATCHING, DELIBERATELY, and this is the important difference
134
+ * from findCatalogItem below. That one is for a PLAYER typing a name and
135
+ * falls back through squashed spellings and word overlap, because
136
+ * "metalink" should find the Meta Link. Here a loose hit would hand a
137
+ * piece of generated street gear some unrelated row's Body and silently
138
+ * resize its hull. A mechanical lookup gets exact slug or exact name and
139
+ * nothing else; anything unrecognised falls through to the documented
140
+ * fallback in models/item.ts.
141
+ *
142
+ * INDEXED, because this is read from a getter that combat touches.
143
+ * loadCatalog() re-reads the cache FILE on every call until a refresh
144
+ * has memoised a live list, so calling it per hull check would put a
145
+ * synchronous read in the middle of a fight.
146
+ */
147
+ export interface ICatalogHull {
148
+ body: number;
149
+ anthro: boolean;
150
+ }
151
+ /** Drop the memo -- called when the catalog itself is replaced. */
152
+ export declare function resetCatalogHullIndex(): void;
153
+ export declare function catalogHullFor(name: string): ICatalogHull | undefined;
26
154
  /** Fuzzy lookup for source.ts: exact slug, exact name, then loose
27
155
  * word containment -- a single unambiguous hit or nothing. */
28
156
  export declare function findCatalogItem(request: string): ICatalogItem | undefined;
157
+ export {};
@@ -36,8 +36,33 @@ const HOSTNAME = DEV ? 'localhost' : 'www.maka-cli.com';
36
36
  const PORT = DEV ? Number(process.env.MAKA_DEV_PORT ?? 3000) : 443;
37
37
  const TIMEOUT_MS = 8000;
38
38
  const CACHE_PATH = path.join(os.homedir(), '.meteor', '.maka', 'side-quest-catalog.json');
39
- let liveCatalog;
39
+ let liveSource;
40
+ let resolved;
40
41
  let fetchAttempted = false;
42
+ /**
43
+ * A VERSION NUMBER, OR NOTHING.
44
+ *
45
+ * `Number(undefined)`, `Number('')` and `Number('1.26.0')` are all NaN,
46
+ * and every comparison against NaN is false -- so an unversioned payload
47
+ * silently loses to whatever it is compared with. That is the SAFE
48
+ * direction (the seed wins) but a silent one, so it is named here and
49
+ * logged at the call site rather than left to fall out of the operator.
50
+ */
51
+ export function parseCatalogVersion(raw) {
52
+ // AN ALLOW-LIST, because `Number()` is far too willing. Written first
53
+ // as `Number.isFinite(Number(raw))` and that was wrong in a way the
54
+ // test caught: Number(null), Number('') and Number([]) are all 0 --
55
+ // a real, comparable, LOWEST-possible version. The payload would then
56
+ // count as versioned, and refreshCatalog would overwrite a good cache
57
+ // with a worthless v0 list, which is precisely the poisoning this
58
+ // exists to stop.
59
+ if (typeof raw !== 'number' && typeof raw !== 'string')
60
+ return undefined;
61
+ if (typeof raw === 'string' && raw.trim() === '')
62
+ return undefined;
63
+ const n = Number(raw);
64
+ return Number.isFinite(n) ? n : undefined;
65
+ }
41
66
  function readCache() {
42
67
  try {
43
68
  const parsed = JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8'));
@@ -56,6 +81,25 @@ function writeCache(version, items) {
56
81
  Logger.getInstance().write(`catalog cache write failed: ${err}`);
57
82
  }
58
83
  }
84
+ /**
85
+ * RECORD WHAT THE NETWORK RETURNED -- separated from the fetch so the
86
+ * wiring is reachable without a socket.
87
+ *
88
+ * This is not a test hatch, it is the step refreshCatalog was doing
89
+ * inline, and pulling it out is what makes the ACTUAL BUG SITE
90
+ * testable: the defect was never in how the candidates compare, it was
91
+ * that loadCatalog returned the live list without ever comparing it.
92
+ * A suite that only exercises chooseCatalog proves the comparison and
93
+ * says nothing about whether anyone calls it -- restoring the original
94
+ * one-line bug left such a suite completely green.
95
+ *
96
+ * An unusable version scores 0: present, comparable, and last.
97
+ */
98
+ export function applyLiveCatalog(items, version) {
99
+ liveSource = { version: version ?? 0, items, origin: 'live' };
100
+ resolved = undefined;
101
+ resetCatalogHullIndex();
102
+ }
59
103
  /** Fire-and-forget refresh from the site -- call once at session start;
60
104
  * the promise resolves when the live list (or a fallback) is settled. */
61
105
  export async function refreshCatalog() {
@@ -77,30 +121,144 @@ export async function refreshCatalog() {
77
121
  });
78
122
  const parsed = JSON.parse(data);
79
123
  if (Array.isArray(parsed?.items) && parsed.items.length > 0) {
80
- liveCatalog = parsed.items;
81
- writeCache(Number(parsed.version ?? CATALOG_VERSION), liveCatalog);
82
- logger.write(`Catalog: ${liveCatalog.length} items from maka-cli.com (v${parsed.version}).`);
124
+ const version = parseCatalogVersion(parsed.version);
125
+ if (version === undefined) {
126
+ // Loud, because it costs the server its authority for this
127
+ // session: an unversioned list cannot be shown to be newer, so
128
+ // the guard below hands the seed to the player instead.
129
+ logger.write(`Catalog: maka-cli.com sent ${parsed.items.length} items with NO usable version `
130
+ + `(${JSON.stringify(parsed.version)}) -- it cannot be compared, so the baked-in v${CATALOG_VERSION} stands.`);
131
+ }
132
+ const fetched = parsed.items;
133
+ applyLiveCatalog(fetched, version);
134
+ if (version !== undefined)
135
+ writeCache(version, fetched);
136
+ // Say what is being SERVED, not merely what arrived. The old line
137
+ // reported the fetch count as though it were the answer, which is
138
+ // how a stale server looked like a healthy one in the log while
139
+ // the player was being handed the seed.
140
+ logger.write(`Catalog: ${fetched.length} items from maka-cli.com (v${parsed.version}); `
141
+ + `serving ${loadCatalog().length}.`);
83
142
  return;
84
143
  }
85
144
  throw new Error('empty catalog payload');
86
145
  }
87
146
  catch (err) {
147
+ // NO ASSIGNMENT HERE ANY MORE. This branch used to set the live
148
+ // list from the cache, which is the same unversioned trust the
149
+ // fetch path was guilty of. loadCatalog reads the cache itself and
150
+ // weighs it, so being offline is now just "no live source".
151
+ resolved = undefined;
152
+ resetCatalogHullIndex();
88
153
  const cache = readCache();
89
154
  if (cache) {
90
- liveCatalog = cache.items;
91
- logger.write(`Catalog: offline -- serving ${cache.items.length} cached items (v${cache.version}). (${err.message})`);
155
+ logger.write(`Catalog: offline -- cache holds ${cache.items.length} items (v${cache.version}); `
156
+ + `serving ${loadCatalog().length}. (${err.message})`);
92
157
  }
93
158
  else {
94
159
  logger.write(`Catalog: offline, no cache -- serving the ${CATALOG_ITEMS.length} baked-in items. (${err.message})`);
95
160
  }
96
161
  }
97
162
  }
98
- /** The best catalog available right now: live > cached > baked-in. */
99
- export function loadCatalog() {
100
- if (liveCatalog)
101
- return liveCatalog;
102
- const cache = readCache();
103
- return cache?.items ?? CATALOG_ITEMS;
163
+ /**
164
+ * The best catalog available right now: live > cached > baked-in --
165
+ * except that a cache OLDER than the shipped seed is not "best", it is
166
+ * stale, and the seed wins.
167
+ *
168
+ * THIS USED TO BE `cache?.items ?? CATALOG_ITEMS`, which handed an
169
+ * upgraded client its old catalog until a refresh happened to succeed.
170
+ * Found while wiring the drone hull lookup: this machine was serving a
171
+ * v6 cache of 198 items with 9 drones while the binary shipped a v10
172
+ * seed of 274 with 70 -- so three merged chunks were invisible offline.
173
+ *
174
+ * IT MATTERS MORE NOW THAN IT DID. When the catalog only fed shop
175
+ * listings, a stale cache meant "you cannot buy the new thing yet".
176
+ * maxDroneBoxes now reads Body from here, so staleness reaches COMBAT
177
+ * MATH: a drone the cache has never heard of falls through to the
178
+ * price-based fallback and is a different toughness than the same drone
179
+ * on a fresh client.
180
+ *
181
+ * A cache NEWER than the seed still wins, which is the whole point of
182
+ * the server being the source of truth.
183
+ *
184
+ * THE GUARD ORIGINALLY COVERED THE WRONG HALF, and the half it missed
185
+ * is the one nearly every player is on. It read:
186
+ *
187
+ * if (liveCatalog) return liveCatalog; // <-- unweighed
188
+ * ...then the version check, on the CACHE only
189
+ *
190
+ * so a FETCH SUCCEEDING was what defeated it. Measured 2026-08-31
191
+ * against the live endpoint: maka-cli.com served v6/198 rows while the
192
+ * shipped seed was v10/274. Online players got the 198 and offline
193
+ * players got the 274 -- being reachable was a DOWNGRADE, and the
194
+ * comment above about combat math was describing a live bug rather than
195
+ * a hypothetical: v6 carries 9 drones against v10's 70, so 61 drones
196
+ * missed catalogHullFor and took the price-based fallback for their
197
+ * condition monitor. Two clients, same drone, different toughness.
198
+ *
199
+ * The fix is to stop trusting ANY source's list because of where it
200
+ * came from, and weigh all three by the only thing that orders them.
201
+ */
202
+ export function loadCatalog(readCacheFn = readCache) {
203
+ if (resolved)
204
+ return resolved.items;
205
+ const cached = readCacheFn();
206
+ const cacheVersion = parseCatalogVersion(cached?.version);
207
+ resolved = chooseCatalog(liveSource, cached && cacheVersion !== undefined
208
+ ? { version: cacheVersion, items: cached.items, origin: 'cache' }
209
+ : undefined);
210
+ return resolved.items;
211
+ }
212
+ /**
213
+ * WHICH OF THE THREE WINS -- pure, and separated out because this is
214
+ * where the bug lived. A decision buried in early returns is one nobody
215
+ * can put a fixture in front of; a named function taking all three
216
+ * candidates can be asked every question directly.
217
+ *
218
+ * Highest version wins. On a TIE the fresher origin wins (live over
219
+ * cache over seed), because equal versions mean equal content and the
220
+ * live list is the one that just came from the source of truth.
221
+ *
222
+ * The seed is not a fallback here, it is a CANDIDATE -- it is the one
223
+ * list whose version is knowable with certainty, since it shipped in
224
+ * the same binary as this function.
225
+ */
226
+ export function chooseCatalog(live, cache) {
227
+ let best = { version: CATALOG_VERSION, items: CATALOG_ITEMS, origin: 'seed' };
228
+ // Ascending freshness, with >= so a later candidate takes a tie.
229
+ for (const candidate of [cache, live]) {
230
+ if (candidate && candidate.items.length > 0 && candidate.version >= best.version) {
231
+ best = candidate;
232
+ }
233
+ }
234
+ return best;
235
+ }
236
+ /** Drop every memo -- the resolved winner and the hull index with it.
237
+ * Exported for tests; production invalidates via refreshCatalog. */
238
+ export function resetCatalogForTest() {
239
+ liveSource = undefined;
240
+ resolved = undefined;
241
+ fetchAttempted = false;
242
+ resetCatalogHullIndex();
243
+ }
244
+ let hullIndex;
245
+ /** Drop the memo -- called when the catalog itself is replaced. */
246
+ export function resetCatalogHullIndex() {
247
+ hullIndex = undefined;
248
+ }
249
+ export function catalogHullFor(name) {
250
+ if (!hullIndex) {
251
+ hullIndex = new Map();
252
+ for (const item of loadCatalog()) {
253
+ const bod = /Bod (\d+)/.exec(item.stats ?? '');
254
+ if (!bod)
255
+ continue;
256
+ const hull = { body: Number(bod[1]), anthro: /anthro:/.test(item.stats ?? '') };
257
+ hullIndex.set(item.slug.toLowerCase(), hull);
258
+ hullIndex.set(item.name.toLowerCase(), hull);
259
+ }
260
+ }
261
+ return hullIndex.get(name.trim().toLowerCase());
104
262
  }
105
263
  /** Fuzzy lookup for source.ts: exact slug, exact name, then loose
106
264
  * word containment -- a single unambiguous hit or nothing. */
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../../../../../../src/commands/game/sideQuest/utilities/catalog.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../../../../../../src/commands/game/sideQuest/utilities/catalog.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,15 @@ export declare function authToken(): string | undefined;
4
4
  * the token) -- the value runner saves BIND to (account-binding
5
5
  * ruling). Never printed; never leaves the machine. */
6
6
  export declare function accountUserId(): string | undefined;
7
- export type CloudResult = 'ok' | 'unauthorized' | 'offline';
7
+ /**
8
+ * `stale` is the server's monotonic guard answering 409: the stored save
9
+ * is NEWER than the one we pushed (§8). Distinct from 'offline' because
10
+ * the two demand opposite responses -- an outage means retry, a stale
11
+ * means STOP pushing and let a human reconcile at the next launch.
12
+ * Folding it into 'offline' would make this client retry forever against
13
+ * a server that is right to refuse it.
14
+ */
15
+ export type CloudResult = 'ok' | 'unauthorized' | 'offline' | 'stale';
8
16
  export interface ICloudSummary {
9
17
  slug: string;
10
18
  playerName: string;
@@ -60,3 +68,133 @@ export declare function reconcile(local: {
60
68
  } | undefined, cloud: {
61
69
  savedAt: string;
62
70
  } | undefined): ReconcileVerdict;
71
+ /**
72
+ * THE COALESCING SINGLE-FLIGHT PUSH QUEUE (server-only storage, PR1).
73
+ *
74
+ * A bound runner's save lives on the server; a beat writes the SPILL
75
+ * synchronously (persistence.spillPathFor) and then enqueues here. Two
76
+ * properties, and both are about not lying to the player:
77
+ *
78
+ * SINGLE-FLIGHT: one request in the air at a time. Overlapping pushes
79
+ * arrive out of order, and last-writer-wins on an out-of-order pair
80
+ * means a later beat can be overwritten by an earlier one that happened
81
+ * to land second -- silently, and only visible as lost progress.
82
+ *
83
+ * COALESCING: the queue holds exactly ONE pending save, the newest.
84
+ * Intermediate snapshots are obsolete the moment a newer one exists, so
85
+ * a slow link must not build a backlog it then has to replay. Play
86
+ * generates beats faster than a bad connection drains them; a FIFO here
87
+ * would guarantee the queue never empties.
88
+ *
89
+ * The spill is what makes both safe: nothing is dropped, because
90
+ * whatever is not confirmed is still on disk.
91
+ */
92
+ /**
93
+ * How a queued save reaches the server. A PARAMETER rather than a hard
94
+ * call to pushSave, because the queue's job is ORDERING and the
95
+ * transport is not part of it -- and because an ES module's exports are
96
+ * read-only, so a test cannot substitute pushSave any other way. The
97
+ * seam follows the shape this codebase already uses for narrow
98
+ * dependencies (IEphemeralHost, attachInputHistory's `render`).
99
+ */
100
+ export type PushFn = (saveFilePath: string, save: ISaveFileV1) => Promise<CloudResult>;
101
+ /** Test seam: the queue is module state, and a suite that ran two cases
102
+ * against one dirty queue would be testing the previous test. */
103
+ export declare function _resetPushQueue(): void;
104
+ /**
105
+ * Enqueue a bound save. Returns immediately -- requestSave's contract is
106
+ * synchronous (game.ts) and the spill has already been written by the
107
+ * time this is called, so nothing here is allowed to block a beat.
108
+ */
109
+ export declare function enqueueBoundPush(saveFilePath: string, save: ISaveFileV1, spillPath?: string, push?: PushFn): void;
110
+ /**
111
+ * Drain to completion -- the QUIT SEAL and the job-accept handoff.
112
+ *
113
+ * These are the two places where "eventually" is not good enough: quit
114
+ * ends the process (nothing later will retry), and accepting a job hands
115
+ * this character to a shared run that loads it from the SERVER, so an
116
+ * unpushed beat is a stale snapshot the whole table then plays against.
117
+ * That race exists today and this is what closes it.
118
+ *
119
+ * Resolves true when the server has the newest save. False means the
120
+ * spill stands and the next launch will carry it.
121
+ */
122
+ export declare function flushPushQueue(): Promise<boolean>;
123
+ /** True while a bound save has not reached the server. */
124
+ export declare function hasPendingPush(): boolean;
125
+ /**
126
+ * Remove a spill whose contents the server has confirmed. Guarded on
127
+ * savedAt: a spill rewritten by a newer beat between the push and the
128
+ * confirmation is NOT the file we just uploaded, and deleting it would
129
+ * throw away the only copy of that beat.
130
+ */
131
+ export declare function clearSpill(spillPath: string, confirmedSavedAt: string): void;
132
+ /**
133
+ * Write the spill, SYNCHRONOUSLY, and never throw.
134
+ *
135
+ * requestSave's signature is synchronous and that is load-bearing: it is
136
+ * called from `process.on('exit')`, where nothing asynchronous can run
137
+ * at all. For a bound runner the spill write IS the durability
138
+ * guarantee at that moment -- the push is best-effort and the process is
139
+ * already leaving.
140
+ *
141
+ * SWALLOWS ITS ERRORS ON PURPOSE, and this is the one place where that
142
+ * is right rather than lazy: a full disk or a permissions fault during
143
+ * the exit hook must not turn a clean quit into a crash on the way out.
144
+ * The player would lose the beat either way; throwing loses the exit
145
+ * too, and prints a stack trace over their last screen.
146
+ */
147
+ export declare function writeSpill(spillPath: string, save: ISaveFileV1): boolean;
148
+ /**
149
+ * ONE SESSION PER RUNNER, ACROSS MACHINES (§8).
150
+ *
151
+ * The local `.lock` already stops two terminals on one box. It cannot
152
+ * see another machine, and without this a second laptop playing the same
153
+ * bound runner is two sessions pushing to one slot -- last-push-wins,
154
+ * with the loser's evening silently gone.
155
+ *
156
+ * TRANSITIONAL TOLERANCE, and it is deliberately loud in the code so it
157
+ * gets removed: the server half of this contract does not exist yet
158
+ * (rollout step 1 ships it; this is step 2). A 404 therefore means
159
+ * "this server does not know about leases", NOT "the lease is free" --
160
+ * and the honest reading of an unimplemented guard is to proceed
161
+ * without it, exactly as this client did before the guard existed.
162
+ *
163
+ * REMOVE THIS BRANCH once the server contract is deployed everywhere.
164
+ * Left in place it would mask a genuinely missing endpoint forever,
165
+ * which is the same shape as the compat branch that hid the review
166
+ * queue's blindness for a day.
167
+ */
168
+ export type LeaseResult = {
169
+ ok: true;
170
+ ttlMs?: number;
171
+ } | {
172
+ ok: false;
173
+ reason: 'held';
174
+ heldBy?: string;
175
+ since?: string;
176
+ } | {
177
+ ok: false;
178
+ reason: 'offline' | 'unauthorized' | 'unsupported';
179
+ };
180
+ export declare const acquireLease: (slug: string, sessionId: string) => Promise<LeaseResult>;
181
+ export declare const releaseLease: (slug: string, sessionId: string) => Promise<LeaseResult>;
182
+ /**
183
+ * Whether a lease failure should STOP the boot.
184
+ *
185
+ * ONLY a held lease does. Offline, unauthorized and unsupported all mean
186
+ * "we could not ask", and refusing to play because the lock server was
187
+ * unreachable would turn every outage into a lockout -- which is the
188
+ * opposite of ruling 2 (an outage degrades to the spill, it does not end
189
+ * the session). A HELD lease is the one case where we know the answer
190
+ * and the answer is no.
191
+ */
192
+ export declare function leaseRefusesBoot(result: LeaseResult): boolean;
193
+ /**
194
+ * Keep the lease alive while playing. Renewal FAILURES ARE TOLERATED:
195
+ * a dropped renew during an outage must not kill a live session -- the
196
+ * server's TTL expires it on its own, and a crash is exactly what the
197
+ * TTL is for.
198
+ */
199
+ export declare function startLeaseRenewal(slug: string, sessionId: string, everyMs?: number): void;
200
+ export declare function stopLeaseRenewal(): void;