@maka/maka-cli 5.65.2 → 5.67.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 (95) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/_index.d.ts +1 -0
  3. package/bundle/typescript/src/commands/game/_index.js +1 -0
  4. package/bundle/typescript/src/commands/game/_index.js.map +1 -1
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/backlog.d.ts +124 -0
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/backlog.js +487 -29
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/backlog.js.map +1 -1
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.d.ts +15 -1
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +61 -4
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js.map +1 -1
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js +4 -1
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js.map +1 -1
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/note.d.ts +73 -14
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/note.js +150 -40
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/note.js.map +1 -1
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/review.d.ts +39 -0
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/review.js +48 -0
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/review.js.map +1 -0
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +4 -1
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -1
  21. package/bundle/typescript/src/commands/game/sideQuest/engine-version.d.ts +1 -1
  22. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +15 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js.map +1 -1
  24. package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.d.ts +87 -0
  25. package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js +320 -0
  26. package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js.map +1 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.d.ts +36 -0
  28. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +116 -3
  29. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
  30. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +8 -1
  31. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js.map +1 -1
  32. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +48 -0
  33. package/bundle/typescript/src/commands/game/sideQuest/game.js +112 -13
  34. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  35. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.d.ts +12 -0
  36. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js +23 -0
  37. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js.map +1 -1
  38. package/bundle/typescript/src/commands/game/sideQuest/headless.js +4 -1
  39. package/bundle/typescript/src/commands/game/sideQuest/headless.js.map +1 -1
  40. package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +32 -0
  41. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +122 -3
  42. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  43. package/bundle/typescript/src/commands/game/sideQuest/types/repro.d.ts +106 -0
  44. package/bundle/typescript/src/commands/game/sideQuest/types/repro.js +2 -0
  45. package/bundle/typescript/src/commands/game/sideQuest/types/repro.js.map +1 -0
  46. package/bundle/typescript/src/commands/game/sideQuest/types/seed/device-seed.d.ts +12 -0
  47. package/bundle/typescript/src/commands/game/sideQuest/ui.d.ts +9 -0
  48. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  49. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-dump.d.ts +17 -1
  50. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-dump.js +250 -36
  51. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-dump.js.map +1 -1
  52. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-review.d.ts +67 -1
  53. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-review.js +99 -0
  54. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-review.js.map +1 -1
  55. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-vocab.d.ts +85 -0
  56. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-vocab.js +100 -0
  57. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-vocab.js.map +1 -0
  58. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog.d.ts +58 -3
  59. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog.js +82 -3
  60. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog.js.map +1 -1
  61. package/bundle/typescript/src/commands/game/sideQuest/utilities/commit-ref.d.ts +13 -0
  62. package/bundle/typescript/src/commands/game/sideQuest/utilities/commit-ref.js +22 -0
  63. package/bundle/typescript/src/commands/game/sideQuest/utilities/commit-ref.js.map +1 -0
  64. package/bundle/typescript/src/commands/game/sideQuest/utilities/handoff.d.ts +42 -0
  65. package/bundle/typescript/src/commands/game/sideQuest/utilities/handoff.js +57 -0
  66. package/bundle/typescript/src/commands/game/sideQuest/utilities/handoff.js.map +1 -0
  67. package/bundle/typescript/src/commands/game/sideQuest/utilities/identity.d.ts +36 -0
  68. package/bundle/typescript/src/commands/game/sideQuest/utilities/identity.js +48 -0
  69. package/bundle/typescript/src/commands/game/sideQuest/utilities/identity.js.map +1 -0
  70. package/bundle/typescript/src/commands/game/sideQuest/utilities/repro-launch.d.ts +68 -0
  71. package/bundle/typescript/src/commands/game/sideQuest/utilities/repro-launch.js +147 -0
  72. package/bundle/typescript/src/commands/game/sideQuest/utilities/repro-launch.js.map +1 -0
  73. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js +55 -1
  74. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js.map +1 -1
  75. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.d.ts +27 -1
  76. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js +44 -11
  77. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js.map +1 -1
  78. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.d.ts +47 -2
  79. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js +76 -8
  80. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js.map +1 -1
  81. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +39 -0
  82. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js.map +1 -1
  83. package/bundle/typescript/src/commands/game/sideQuest/utilities/table-notes.d.ts +30 -0
  84. package/bundle/typescript/src/commands/game/sideQuest/utilities/table-notes.js +46 -0
  85. package/bundle/typescript/src/commands/game/sideQuest/utilities/table-notes.js.map +1 -0
  86. package/bundle/typescript/src/commands/game/sideQuest-backlog.sub.cmd.d.ts +1 -0
  87. package/bundle/typescript/src/commands/game/sideQuest-backlog.sub.cmd.js +46 -0
  88. package/bundle/typescript/src/commands/game/sideQuest-backlog.sub.cmd.js.map +1 -0
  89. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.d.ts +3 -1
  90. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +269 -42
  91. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
  92. package/bundle/typescript/src/tools/log/log.class.d.ts +17 -0
  93. package/bundle/typescript/src/tools/log/log.class.js +30 -1
  94. package/bundle/typescript/src/tools/log/log.class.js.map +1 -1
  95. package/package.json +1 -1
@@ -0,0 +1,320 @@
1
+ import { Direction } from '../types/shared/direction-enum.js';
2
+ import { ARCHETYPES, getArchetype } from '../archetypes.js';
3
+ /**
4
+ * BUILDING THE BENCH A REVIEW ITEM SHIPS WITH.
5
+ *
6
+ * Two rooms and one sealed door between them. That is the whole scene,
7
+ * on purpose: a repro is a bench, not a run, and every room you have to
8
+ * walk through to reach the thing under test is a room the reproduction
9
+ * can go wrong in.
10
+ *
11
+ * THIS FILE OWNS THE INVARIANTS, and that is the reason it exists
12
+ * rather than the specs carrying raw seed JSON. The author of a repro
13
+ * states INTENT -- "a guard in the doorway", "a keypad sealing north" --
14
+ * and everything that intent implies is derived here, once, for every
15
+ * repro that will ever be written.
16
+ *
17
+ * The invariant that motivated the design: an NPC marked `guarding`
18
+ * whose startSpot is not the sealed exit's spot. Nothing throws.
19
+ * `holdsGround` is simply never consulted, because doorwayOccupants
20
+ * reads the exit cell and the guard is not standing in it. The scene
21
+ * builds, the walkthrough passes, and the repro demonstrates the
22
+ * opposite of what it claims -- a check whose two sides fail together
23
+ * cannot see the failure, which is the dominant defect shape in this
24
+ * codebase's history. An author cannot make that mistake here, because
25
+ * an author never supplies the spot.
26
+ *
27
+ * The other invariants are refusals rather than derivations, and they
28
+ * refuse LOUDLY rather than degrading. A repro that silently does less
29
+ * than it says is the failure mode this whole feature is built against,
30
+ * so "drop the field and carry on" -- which is what the runtime does
31
+ * with a code on a lock -- is exactly the wrong behaviour here.
32
+ */
33
+ /** The room the device stands in, which is also the room that declares
34
+ * the exit it seals. */
35
+ export const REPRO_BARRIER_ROOM = 'The Approach';
36
+ /** Through the door. */
37
+ export const REPRO_FAR_ROOM = 'The Far Side';
38
+ /** The one spot that matters, and the one an author never has to name.
39
+ * Both rooms have it: a doorway has two sides, and a guard posted on
40
+ * the far side stands in their own. */
41
+ export const REPRO_DOORWAY_SPOT = 'doorway';
42
+ /**
43
+ * Somewhere to stand that is NOT the doorway, so "move to the door" is
44
+ * a real move with a real before-state. Without it the runner starts in
45
+ * the doorway and the whole class of "I walked onto the guard's cell"
46
+ * items cannot be reproduced at all.
47
+ *
48
+ * NOT called "open floor", which was the first name and was wrong for a
49
+ * reason worth recording: "open floor" is the ENGINE's phrase for
50
+ * having no named spot at all (see move.ts -- "You peel off to the open
51
+ * floor"). Naming a real spot that made a positioned NPC render as
52
+ * "Grier at the open floor, Grier on the open floor" -- one clause from
53
+ * the spot, one from the generic fallback, reading exactly like a
54
+ * duplicate-render bug. Every reviewer sent to a bench would have filed
55
+ * it. A fixture must not borrow the engine's words for "none".
56
+ */
57
+ export const REPRO_FLOOR_SPOT = 'back wall';
58
+ const DEFAULT_DEVICE_NAME = {
59
+ lock: 'Maglock',
60
+ panel: 'Keypad',
61
+ ward: 'Chalk Ward',
62
+ };
63
+ const DEFAULT_DEVICE_DESCRIPTION = {
64
+ lock: 'A heavy maglock, seated in the frame and holding the door shut.',
65
+ panel: 'A keypad on a short steel stalk, its display waiting for a code.',
66
+ ward: 'A ward drawn in chalk and old ash across the whole doorway, humming against your teeth.',
67
+ };
68
+ const OPPOSITE = {
69
+ [Direction.NORTH]: Direction.SOUTH,
70
+ [Direction.SOUTH]: Direction.NORTH,
71
+ [Direction.EAST]: Direction.WEST,
72
+ [Direction.WEST]: Direction.EAST,
73
+ [Direction.UP]: Direction.DOWN,
74
+ [Direction.DOWN]: Direction.UP,
75
+ };
76
+ /** Archetypes that can work a ward. `dispel` needs the Art; nothing
77
+ * else in the game does. */
78
+ const AWAKENED = new Set(['Adept', 'Combat Mage', 'Street Shaman']);
79
+ /** Bounds. A repro is a bench: more than this and it is a scene, and a
80
+ * scene is a place a reproduction gets lost in. */
81
+ const MAX_NPCS = 4;
82
+ const MAX_ITEMS = 6;
83
+ /** A fixture that cannot be built into an honest bench. Thrown, never
84
+ * swallowed -- the caller turns it into a sentence for whoever wrote
85
+ * the spec. */
86
+ export class ReproFixtureError extends Error {
87
+ constructor(message) {
88
+ super(message);
89
+ this.name = 'ReproFixtureError';
90
+ }
91
+ }
92
+ function roomFor(side) {
93
+ return side === 'far' ? REPRO_FAR_ROOM : REPRO_BARRIER_ROOM;
94
+ }
95
+ /** A guard who is not in the doorway guards nothing, so `guarding`
96
+ * implies `atDoorway` rather than sitting beside it as a second flag
97
+ * the two could disagree about. */
98
+ function standsInDoorway(npc) {
99
+ return npc.guarding === true || npc.atDoorway === true;
100
+ }
101
+ export function buildReproScene(fixture) {
102
+ const warnings = [];
103
+ // ARCHETYPE. getArchetype falls back to ARCHETYPES[0] on a name it
104
+ // does not know, which is right for a launcher menu and wrong here:
105
+ // a ward repro silently handed a mundane Runner is a repro that
106
+ // cannot be run, and it would look like the fix had failed.
107
+ const archetypeName = fixture.archetype ?? 'Covert Ops';
108
+ if (!ARCHETYPES.some(a => a.name === archetypeName)) {
109
+ throw new ReproFixtureError(`No archetype called "${archetypeName}". One of: ${ARCHETYPES.map(a => a.name).join(', ')}.`);
110
+ }
111
+ const npcs = fixture.npcs ?? [];
112
+ const items = fixture.items ?? [];
113
+ if (npcs.length > MAX_NPCS) {
114
+ throw new ReproFixtureError(`A repro carries at most ${MAX_NPCS} NPCs; this one has ${npcs.length}.`);
115
+ }
116
+ if (items.length > MAX_ITEMS) {
117
+ throw new ReproFixtureError(`A repro carries at most ${MAX_ITEMS} items; this one has ${items.length}.`);
118
+ }
119
+ const duplicateNpc = npcs.find((n, i) => npcs.findIndex(o => o.name === n.name) !== i);
120
+ if (duplicateNpc) {
121
+ throw new ReproFixtureError(`Two NPCs are both called "${duplicateNpc.name}" -- a name has to pick one body out.`);
122
+ }
123
+ const barrier = fixture.barrier;
124
+ const direction = barrier?.sealsExit ?? Direction.NORTH;
125
+ const startSide = fixture.start ?? 'barrier';
126
+ const startRoom = roomFor(startSide);
127
+ let device;
128
+ if (barrier) {
129
+ // INVARIANT 1: a code belongs to a panel and nowhere else. The
130
+ // Device constructor DROPS it on a lock or a ward, silently and
131
+ // correctly -- that is the runtime honouring "you can't talk at a
132
+ // mechanical lock". Inheriting that drop here would produce a bench
133
+ // whose stated credential does nothing, and the reviewer would read
134
+ // the refusal as the bug still being present.
135
+ if (barrier.code && barrier.kind !== 'panel') {
136
+ throw new ReproFixtureError(`A ${barrier.kind} has no code -- only a panel does. Give it a keyItem, or let it be picked or dispelled.`);
137
+ }
138
+ // A panel with no code cannot be opened the way a panel opens.
139
+ if (barrier.kind === 'panel' && !barrier.code && !barrier.keyItem && !barrier.open) {
140
+ warnings.push('This panel has neither a code nor a key, so the only way through it is force.');
141
+ }
142
+ if (barrier.kind === 'ward' && !AWAKENED.has(archetypeName)) {
143
+ warnings.push(`A ward answers to the Art, and ${archetypeName} is mundane -- fine if the refusal is the point, otherwise use Combat Mage.`);
144
+ }
145
+ device = {
146
+ name: barrier.name ?? DEFAULT_DEVICE_NAME[barrier.kind],
147
+ description: DEFAULT_DEVICE_DESCRIPTION[barrier.kind],
148
+ kind: barrier.kind,
149
+ code: barrier.code,
150
+ keyItem: barrier.keyItem,
151
+ // INVARIANT 2: the device stands AT the doorway it seals, so
152
+ // reaching it is reaching the door. bypass-command resolves the
153
+ // spot as exitSpot(room, opensExit) ?? atSpot, and a device
154
+ // sitting at neither is a device you cannot walk up to.
155
+ atSpot: REPRO_DOORWAY_SPOT,
156
+ // INVARIANT 3: naming the exit is what locks the door.
157
+ // scene-factory only LOGS when a device names something that is
158
+ // not a door, which on a bench means an unsealed doorway and a
159
+ // reviewer concluding the barrier never worked.
160
+ opensExit: direction,
161
+ // The walkthrough test caught this being dropped, which is the
162
+ // point of having one: `open: true` was accepted here and went
163
+ // nowhere, because the seed type had no such field. A fixture
164
+ // that quietly does less than it says is the exact failure this
165
+ // whole feature exists to prevent -- so the field was added to
166
+ // IDeviceSeedJson rather than the intent silently discarded.
167
+ open: barrier.open === true,
168
+ };
169
+ }
170
+ const spots = [
171
+ { name: REPRO_DOORWAY_SPOT },
172
+ { name: REPRO_FLOOR_SPOT },
173
+ ];
174
+ // THE BARRIER IS NAMED IN THE PROSE, and where it stands, and which
175
+ // way the door runs.
176
+ //
177
+ // A device is only surfaced by `search`, which is right for a real
178
+ // scene -- finding things is play. On a bench it is a trap: the whole
179
+ // promise is "you start standing at the thing", and the first player
180
+ // through one reported exactly that ("There is no location for the
181
+ // door, so I cannot get to it"). They looked, saw two spots and no
182
+ // door, and concluded the fixture was broken. Nothing here should
183
+ // have to be discovered.
184
+ const barrierRoom = {
185
+ name: REPRO_BARRIER_ROOM,
186
+ description: barrier
187
+ ? `A short concrete run ending at one door, ${direction} of you. `
188
+ + `${device.name} is fixed beside the doorway${barrier.open ? ', standing open' : ', holding it shut'}. `
189
+ + `There is nothing else here, which is the point -- whatever is wrong is wrong at that door.`
190
+ : `A short concrete run with a door ${direction} of you and nothing else in it.`,
191
+ roomType: 'Street',
192
+ size: 'medium',
193
+ spots,
194
+ isStartRoom: startSide === 'barrier',
195
+ exits: [{ direction, targetRoom: REPRO_FAR_ROOM, atSpot: REPRO_DOORWAY_SPOT }],
196
+ devices: device ? [device] : undefined,
197
+ };
198
+ const farRoom = {
199
+ name: REPRO_FAR_ROOM,
200
+ description: `The other side of the door, which lies ${OPPOSITE[direction]} of you`
201
+ + `${barrier ? ` and is held from the far side by ${device.name}` : ''}. Bare, and no further out.`,
202
+ roomType: 'Street',
203
+ size: 'medium',
204
+ spots,
205
+ isStartRoom: startSide === 'far',
206
+ // INVARIANT 3, other half: a door has two sides. Without the return
207
+ // exit the far room is a one-way pocket, and "go back and try it
208
+ // again" -- which is most of what reviewing a fix consists of --
209
+ // would not work.
210
+ exits: [{ direction: OPPOSITE[direction], targetRoom: REPRO_BARRIER_ROOM, atSpot: REPRO_DOORWAY_SPOT }],
211
+ };
212
+ const npcSeeds = npcs.map(npc => {
213
+ const side = npc.side ?? 'barrier';
214
+ const inDoorway = standsInDoorway(npc);
215
+ return {
216
+ description: npc.concept,
217
+ dialogue: [],
218
+ player: {
219
+ name: npc.name,
220
+ startLocation: roomFor(side),
221
+ // THE INVARIANT THIS FILE EXISTS FOR. Derived from the
222
+ // direction the barrier seals, never supplied -- a guarding NPC
223
+ // standing anywhere else holds nothing, silently, and the
224
+ // walkthrough would still pass.
225
+ startSpot: inDoorway ? REPRO_DOORWAY_SPOT : REPRO_FLOOR_SPOT,
226
+ guarding: npc.guarding === true,
227
+ lifestyleChoices: [],
228
+ combat: {
229
+ body: 4, agility: 4, reaction: 4, combatSkill: 3, strength: 4,
230
+ willpower: 3, logic: 3, intuition: 3, charisma: 3, edge: 2,
231
+ skills: { unarmed: 3, perception: 3, negotiation: 3 },
232
+ },
233
+ },
234
+ };
235
+ });
236
+ const itemSeeds = items.map(it => ({
237
+ name: it.name,
238
+ description: it.description,
239
+ details: it.details,
240
+ size: 'Tiny',
241
+ category: 'Document',
242
+ shape: 'Rectangle',
243
+ color: 'Gray',
244
+ texture: 'Plain',
245
+ rating: 'Common',
246
+ weight: 0.1,
247
+ room: roomFor(it.side ?? 'barrier'),
248
+ atSpot: REPRO_FLOOR_SPOT,
249
+ }));
250
+ // INVARIANT 4: the key is on the side the runner starts on. A key
251
+ // behind the door it opens is a bench nobody can work, and it reads
252
+ // as the fix having failed rather than the fixture being wrong.
253
+ if (barrier?.keyItem) {
254
+ itemSeeds.push({
255
+ name: barrier.keyItem,
256
+ description: `The thing that opens the ${device?.name ?? 'door'}.`,
257
+ size: 'Tiny',
258
+ category: 'Key',
259
+ shape: 'Rectangle',
260
+ color: 'Steel',
261
+ texture: 'Milled',
262
+ rating: 'Common',
263
+ weight: 0.1,
264
+ room: startRoom,
265
+ atSpot: REPRO_FLOOR_SPOT,
266
+ });
267
+ }
268
+ if (barrier && !barrier.open && startSide === 'far') {
269
+ warnings.push(`The runner starts on the far side, so the ${device?.name} is through a shut door and cannot be worked from there -- right for testing what someone SAYS about the door, wrong for testing the door.`);
270
+ }
271
+ const seed = {
272
+ name: 'Repro Bench',
273
+ story: fixture.premise
274
+ ?? 'A bench, not a job. One door, whatever is standing at it, and nothing else to be distracted by.',
275
+ currency: { type: 'nuyen', authorityRoomTags: [] },
276
+ residenceTiers: ['Street', 'Squatter', 'Low'],
277
+ rooms: [barrierRoom, farRoom],
278
+ npcs: npcSeeds,
279
+ items: itemSeeds,
280
+ puzzles: [],
281
+ player: {
282
+ name: 'Testbed',
283
+ startLocation: startRoom,
284
+ // NO RESIDENCE. Copied from paritySeed originally, where it is
285
+ // right -- that fixture models a real run. A bench models nothing
286
+ // and nobody lives in it; declaring a home here only invites the
287
+ // lifestyle machinery to have opinions about a two-room scene.
288
+ lifestyleChoices: [],
289
+ startingItems: [],
290
+ },
291
+ // NO winCondition AND NO client, deliberately. A bench cannot be
292
+ // won, because there is nothing to win -- adding a payout would put
293
+ // a wrap-up, a settlement and an offer of more work in the way of a
294
+ // reviewer who came to look at one door. scene1 already proves a
295
+ // scene boots without a client.
296
+ };
297
+ return {
298
+ seed,
299
+ names: {
300
+ device: device?.name,
301
+ npcs: npcs.map(n => n.name),
302
+ direction,
303
+ barrierRoom: REPRO_BARRIER_ROOM,
304
+ farRoom: REPRO_FAR_ROOM,
305
+ startRoom,
306
+ },
307
+ warnings,
308
+ };
309
+ }
310
+ /** The archetype a fixture asks for, resolved. Separate from the build
311
+ * because the launcher hands the NAME to the game (which rolls the
312
+ * character itself) while the headless walkthrough needs the spread to
313
+ * make a save out of. */
314
+ export function reproArchetypeName(fixture) {
315
+ return fixture.archetype ?? 'Covert Ops';
316
+ }
317
+ export function reproArchetype(fixture) {
318
+ return getArchetype(reproArchetypeName(fixture));
319
+ }
320
+ //# sourceMappingURL=repro-scene.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repro-scene.js","sourceRoot":"","sources":["../../../../../../../src/commands/game/sideQuest/factories/repro-scene.ts"],"names":[],"mappings":"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"}
@@ -328,6 +328,42 @@ export declare function resolveSpotName(declared: string, spotNames: string[]):
328
328
  * type.
329
329
  */
330
330
  export declare function ensureCredentialKindAgrees(details: ISceneDetails): string[];
331
+ /**
332
+ * RESOLVE A NAME ONE LIST WROTE AGAINST THE LIST THAT OWNS IT.
333
+ *
334
+ * The skeleton cross-references by NAME in two places, and both are
335
+ * written by the model in different passes: an exit names the `device`
336
+ * that holds it, and an exit names the `guard` who stands in it. Both
337
+ * were then matched by exact string equality against devices[].name and
338
+ * npcs[].name -- and a near-miss produced no error, no warning, and a
339
+ * scene that validated perfectly while meaning nothing:
340
+ *
341
+ * exit device "Keypad" vs devices[] "Door Keypad"
342
+ * -> the device gets no opensExit
343
+ * -> scene-factory never locks the door
344
+ * -> "there is nothing this keypad locks, the door is open"
345
+ *
346
+ * exit guard "Wraith" vs npcs[] "Wraith Kovac"
347
+ * -> the NPC never gets `guarding`
348
+ * -> holdsGround sees a bystander, yieldDoorway moves them aside
349
+ * -> "I was able to just, push Wraith away"
350
+ *
351
+ * Two player-visible mechanics, one shape: a reference validated for
352
+ * STRUCTURE (a string is present) and never for MEANING (it names
353
+ * something that exists).
354
+ *
355
+ * REPAIR, not rejection -- the pattern this file already uses for a
356
+ * missing clue phrase and a guarded exit with no spot. Failing the
357
+ * draft costs a 6,000-token regeneration to fix one word, and a warning
358
+ * alone leaves the player-visible wrong in place.
359
+ *
360
+ * DELIBERATELY CONSERVATIVE. Exact, then trimmed/case-insensitive, then
361
+ * a UNIQUE substring either way. No typo tolerance and no nearest
362
+ * guess: binding a door to the wrong device is worse than leaving it
363
+ * unbound, because the unbound case at least logs. Anything that does
364
+ * not resolve returns undefined and says so.
365
+ */
366
+ export declare function resolveNamedRef(ref: string, candidates: string[], what: string): string | undefined;
331
367
  export declare function assembleScene(skeleton: ISceneSkeleton, details: ISceneDetails, playerName: string): ISceneSeedJson;
332
368
  export type RepairTarget = 'rooms' | 'npcs' | 'devices' | 'items' | 'skeleton';
333
369
  /**
@@ -1174,6 +1174,38 @@ function resolveDirectionCollisions(skeleton) {
1174
1174
  */
1175
1175
  export function normalizeSkeleton(skeleton) {
1176
1176
  const notes = [];
1177
+ // WHO THE CLIENT IS, before anything counts them.
1178
+ //
1179
+ // NOT a missing guard -- an audit of the four names the model writes
1180
+ // about other lists (2026-08-28) found all four checked: heldBy,
1181
+ // winCondition.toNpc and .item fail in validateSkeleton below, and
1182
+ // `client` fails in validateClient. The gap is narrower and it is
1183
+ // about COST and ORDER.
1184
+ //
1185
+ // COST: validateClient REJECTS an unresolvable client, so a one-word
1186
+ // near miss -- "Mr. Osprey" against a roster saying "Osprey" -- burns
1187
+ // a full regeneration to fix a name the skeleton already contains.
1188
+ // Every other name reference gained a repair on 2026-08-27; this one
1189
+ // did not, so it is the only near miss still paying full price.
1190
+ //
1191
+ // ORDER: the opposition floor counts hostiles as
1192
+ // `npcs.filter(n => n.name !== skeleton.client)` in validateSkeleton,
1193
+ // which runs BEFORE validateClient ever sees the assembled scene. So
1194
+ // an unrepaired near miss counts the CLIENT as a hostile and skews
1195
+ // the difficulty floor by one -- silently, and in a check whose whole
1196
+ // job is to notice an under-populated cast.
1197
+ //
1198
+ // Repairing here fixes both: the count is right, and a resolvable
1199
+ // name never reaches the rejection. What genuinely matches nothing
1200
+ // still falls to validateClient, which owns that verdict and words
1201
+ // it better than a duplicate here would.
1202
+ if (skeleton.client) {
1203
+ const canonicalClient = resolveNamedRef(skeleton.client, (skeleton.npcs ?? []).map(n => n.name), 'the scene-level "client"');
1204
+ if (canonicalClient && canonicalClient !== skeleton.client) {
1205
+ notes.push(`client "${skeleton.client}" renamed to "${canonicalClient}" to match the roster.`);
1206
+ skeleton.client = canonicalClient;
1207
+ }
1208
+ }
1177
1209
  // Topology before anything that reasons about topology: freeStranded-
1178
1210
  // Rooms, clue placement and the curb all walk this graph, and they
1179
1211
  // should walk the one the ENGINE will actually build.
@@ -1558,6 +1590,66 @@ export function ensureCredentialKindAgrees(details) {
1558
1590
  }
1559
1591
  return notes;
1560
1592
  }
1593
+ /**
1594
+ * RESOLVE A NAME ONE LIST WROTE AGAINST THE LIST THAT OWNS IT.
1595
+ *
1596
+ * The skeleton cross-references by NAME in two places, and both are
1597
+ * written by the model in different passes: an exit names the `device`
1598
+ * that holds it, and an exit names the `guard` who stands in it. Both
1599
+ * were then matched by exact string equality against devices[].name and
1600
+ * npcs[].name -- and a near-miss produced no error, no warning, and a
1601
+ * scene that validated perfectly while meaning nothing:
1602
+ *
1603
+ * exit device "Keypad" vs devices[] "Door Keypad"
1604
+ * -> the device gets no opensExit
1605
+ * -> scene-factory never locks the door
1606
+ * -> "there is nothing this keypad locks, the door is open"
1607
+ *
1608
+ * exit guard "Wraith" vs npcs[] "Wraith Kovac"
1609
+ * -> the NPC never gets `guarding`
1610
+ * -> holdsGround sees a bystander, yieldDoorway moves them aside
1611
+ * -> "I was able to just, push Wraith away"
1612
+ *
1613
+ * Two player-visible mechanics, one shape: a reference validated for
1614
+ * STRUCTURE (a string is present) and never for MEANING (it names
1615
+ * something that exists).
1616
+ *
1617
+ * REPAIR, not rejection -- the pattern this file already uses for a
1618
+ * missing clue phrase and a guarded exit with no spot. Failing the
1619
+ * draft costs a 6,000-token regeneration to fix one word, and a warning
1620
+ * alone leaves the player-visible wrong in place.
1621
+ *
1622
+ * DELIBERATELY CONSERVATIVE. Exact, then trimmed/case-insensitive, then
1623
+ * a UNIQUE substring either way. No typo tolerance and no nearest
1624
+ * guess: binding a door to the wrong device is worse than leaving it
1625
+ * unbound, because the unbound case at least logs. Anything that does
1626
+ * not resolve returns undefined and says so.
1627
+ */
1628
+ export function resolveNamedRef(ref, candidates, what) {
1629
+ const want = ref.trim().toLowerCase();
1630
+ if (!want)
1631
+ return undefined;
1632
+ const exact = candidates.find(c => c === ref);
1633
+ if (exact)
1634
+ return exact;
1635
+ const loose = candidates.filter(c => c.trim().toLowerCase() === want);
1636
+ if (loose.length === 1)
1637
+ return loose[0];
1638
+ const partial = candidates.filter(c => {
1639
+ const n = c.trim().toLowerCase();
1640
+ return n.includes(want) || want.includes(n);
1641
+ });
1642
+ if (partial.length === 1)
1643
+ return partial[0];
1644
+ // Unresolvable, or ambiguous between two candidates. Say so loudly --
1645
+ // this is the line that would have turned two evenings of "pretty
1646
+ // poor" into one grep.
1647
+ try {
1648
+ Logger.getInstance().write(`assembleScene: ${what} names "${ref}", which matches ${partial.length === 0 ? 'nothing' : `${partial.length} things`} in [${candidates.join(', ')}] -- the reference is dropped and the mechanic will not fire.`);
1649
+ }
1650
+ catch { /* no session: the note is a nicety, not a contract */ }
1651
+ return undefined;
1652
+ }
1561
1653
  export function assembleScene(skeleton, details, playerName) {
1562
1654
  // The answer has to be legible before anything is assembled from it.
1563
1655
  ensureClueDetails(skeleton, details);
@@ -1754,7 +1846,18 @@ export function assembleScene(skeleton, details, playerName) {
1754
1846
  }
1755
1847
  catch { /* no session: the note is a nicety, not a contract */ }
1756
1848
  }
1757
- guardPost.set(named, { room: room.name, spot: e.atSpot });
1849
+ // CANONICALISE, exactly as heldExit does above and for the same
1850
+ // reason. `guardPost` is keyed by the exit's guard string and
1851
+ // read back by the NPC's own name, so an exit naming "Wraith"
1852
+ // while the roster says "Wraith Kovac" posted a guard nobody
1853
+ // matched: `guarding` never got set, holdsGround saw an ordinary
1854
+ // bystander, and yieldDoorway politely moved them aside.
1855
+ // Reported verbatim: "I was able to just, push Wraith away by
1856
+ // moving to the vault door."
1857
+ const canonicalGuard = resolveNamedRef(named, skeleton.npcs.map(n => n.name), `guard on ${room.name}'s ${e.direction} exit`);
1858
+ if (!canonicalGuard)
1859
+ continue;
1860
+ guardPost.set(canonicalGuard, { room: room.name, spot: e.atSpot });
1758
1861
  }
1759
1862
  }
1760
1863
  const npcs = skeleton.npcs.map(npc => {
@@ -1830,8 +1933,18 @@ export function assembleScene(skeleton, details, playerName) {
1830
1933
  const heldExit = new Map();
1831
1934
  for (const room of skeleton.rooms ?? []) {
1832
1935
  for (const e of room.exits ?? []) {
1833
- if (e.device)
1834
- heldExit.set(e.device, e.direction);
1936
+ if (!e.device)
1937
+ continue;
1938
+ // CANONICALISE THE REFERENCE, or the door never locks. See
1939
+ // resolveNamedRef: an exit saying device "Keypad" while devices[]
1940
+ // calls it "Door Keypad" used to produce a keypad with no
1941
+ // opensExit, and scene-factory locks a door FROM its device's
1942
+ // opensExit -- so the player met a keypad beside a door standing
1943
+ // open. Reported verbatim: "there is nothing this keypad locks,
1944
+ // the door is open."
1945
+ const canonical = resolveNamedRef(e.device, (skeleton.devices ?? []).map(d => d.name), `device on ${room.name}'s ${e.direction} exit`);
1946
+ if (canonical)
1947
+ heldExit.set(canonical, e.direction);
1835
1948
  }
1836
1949
  }
1837
1950
  const devices = (skeleton.devices ?? []).map(device => {