@sharpee/transcript-tester 0.9.61-beta

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 (137) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.d.ts +11 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +367 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/condition-evaluator.d.ts +30 -0
  8. package/dist/condition-evaluator.d.ts.map +1 -0
  9. package/dist/condition-evaluator.js +314 -0
  10. package/dist/condition-evaluator.js.map +1 -0
  11. package/dist/fast-cli.d.ts +13 -0
  12. package/dist/fast-cli.d.ts.map +1 -0
  13. package/dist/fast-cli.js +363 -0
  14. package/dist/fast-cli.js.map +1 -0
  15. package/dist/index.d.ts +17 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +48 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/navigator.d.ts +27 -0
  20. package/dist/navigator.d.ts.map +1 -0
  21. package/dist/navigator.js +303 -0
  22. package/dist/navigator.js.map +1 -0
  23. package/dist/parser.d.ts +19 -0
  24. package/dist/parser.d.ts.map +1 -0
  25. package/dist/parser.js +453 -0
  26. package/dist/parser.js.map +1 -0
  27. package/dist/reporter.d.ts +41 -0
  28. package/dist/reporter.d.ts.map +1 -0
  29. package/dist/reporter.js +386 -0
  30. package/dist/reporter.js.map +1 -0
  31. package/dist/runner.d.ts +44 -0
  32. package/dist/runner.d.ts.map +1 -0
  33. package/dist/runner.js +977 -0
  34. package/dist/runner.js.map +1 -0
  35. package/dist/story-loader.d.ts +31 -0
  36. package/dist/story-loader.d.ts.map +1 -0
  37. package/dist/story-loader.js +169 -0
  38. package/dist/story-loader.js.map +1 -0
  39. package/dist/types.d.ts +204 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +8 -0
  42. package/dist/types.js.map +1 -0
  43. package/dist-esm/cli.d.ts +11 -0
  44. package/dist-esm/cli.d.ts.map +1 -0
  45. package/dist-esm/cli.js +332 -0
  46. package/dist-esm/cli.js.map +1 -0
  47. package/dist-esm/condition-evaluator.d.ts +30 -0
  48. package/dist-esm/condition-evaluator.d.ts.map +1 -0
  49. package/dist-esm/condition-evaluator.js +311 -0
  50. package/dist-esm/condition-evaluator.js.map +1 -0
  51. package/dist-esm/fast-cli.d.ts +13 -0
  52. package/dist-esm/fast-cli.d.ts.map +1 -0
  53. package/dist-esm/fast-cli.js +328 -0
  54. package/dist-esm/fast-cli.js.map +1 -0
  55. package/dist-esm/index.d.ts +17 -0
  56. package/dist-esm/index.d.ts.map +1 -0
  57. package/dist-esm/index.js +21 -0
  58. package/dist-esm/index.js.map +1 -0
  59. package/dist-esm/navigator.d.ts +27 -0
  60. package/dist-esm/navigator.d.ts.map +1 -0
  61. package/dist-esm/navigator.js +300 -0
  62. package/dist-esm/navigator.js.map +1 -0
  63. package/dist-esm/parser.d.ts +19 -0
  64. package/dist-esm/parser.d.ts.map +1 -0
  65. package/dist-esm/parser.js +415 -0
  66. package/dist-esm/parser.js.map +1 -0
  67. package/dist-esm/reporter.d.ts +41 -0
  68. package/dist-esm/reporter.d.ts.map +1 -0
  69. package/dist-esm/reporter.js +342 -0
  70. package/dist-esm/reporter.js.map +1 -0
  71. package/dist-esm/runner.d.ts +44 -0
  72. package/dist-esm/runner.d.ts.map +1 -0
  73. package/dist-esm/runner.js +941 -0
  74. package/dist-esm/runner.js.map +1 -0
  75. package/dist-esm/story-loader.d.ts +31 -0
  76. package/dist-esm/story-loader.d.ts.map +1 -0
  77. package/dist-esm/story-loader.js +131 -0
  78. package/dist-esm/story-loader.js.map +1 -0
  79. package/dist-esm/types.d.ts +204 -0
  80. package/dist-esm/types.d.ts.map +1 -0
  81. package/dist-esm/types.js +7 -0
  82. package/dist-esm/types.js.map +1 -0
  83. package/dist-npm/cli.d.ts +11 -0
  84. package/dist-npm/cli.d.ts.map +1 -0
  85. package/dist-npm/cli.js +367 -0
  86. package/dist-npm/cli.js.map +1 -0
  87. package/dist-npm/condition-evaluator.d.ts +30 -0
  88. package/dist-npm/condition-evaluator.d.ts.map +1 -0
  89. package/dist-npm/condition-evaluator.js +314 -0
  90. package/dist-npm/condition-evaluator.js.map +1 -0
  91. package/dist-npm/fast-cli.d.ts +13 -0
  92. package/dist-npm/fast-cli.d.ts.map +1 -0
  93. package/dist-npm/fast-cli.js +363 -0
  94. package/dist-npm/fast-cli.js.map +1 -0
  95. package/dist-npm/index.d.ts +17 -0
  96. package/dist-npm/index.d.ts.map +1 -0
  97. package/dist-npm/index.js +48 -0
  98. package/dist-npm/index.js.map +1 -0
  99. package/dist-npm/navigator.d.ts +27 -0
  100. package/dist-npm/navigator.d.ts.map +1 -0
  101. package/dist-npm/navigator.js +303 -0
  102. package/dist-npm/navigator.js.map +1 -0
  103. package/dist-npm/parser.d.ts +19 -0
  104. package/dist-npm/parser.d.ts.map +1 -0
  105. package/dist-npm/parser.js +453 -0
  106. package/dist-npm/parser.js.map +1 -0
  107. package/dist-npm/reporter.d.ts +41 -0
  108. package/dist-npm/reporter.d.ts.map +1 -0
  109. package/dist-npm/reporter.js +386 -0
  110. package/dist-npm/reporter.js.map +1 -0
  111. package/dist-npm/runner.d.ts +44 -0
  112. package/dist-npm/runner.d.ts.map +1 -0
  113. package/dist-npm/runner.js +977 -0
  114. package/dist-npm/runner.js.map +1 -0
  115. package/dist-npm/story-loader.d.ts +31 -0
  116. package/dist-npm/story-loader.d.ts.map +1 -0
  117. package/dist-npm/story-loader.js +169 -0
  118. package/dist-npm/story-loader.js.map +1 -0
  119. package/dist-npm/types.d.ts +204 -0
  120. package/dist-npm/types.d.ts.map +1 -0
  121. package/dist-npm/types.js +8 -0
  122. package/dist-npm/types.js.map +1 -0
  123. package/package.json +49 -0
  124. package/src/cli.ts +385 -0
  125. package/src/condition-evaluator.ts +382 -0
  126. package/src/fast-cli.ts +403 -0
  127. package/src/index.ts +26 -0
  128. package/src/navigator.ts +365 -0
  129. package/src/parser.ts +488 -0
  130. package/src/reporter.ts +409 -0
  131. package/src/runner.ts +1152 -0
  132. package/src/story-loader.ts +168 -0
  133. package/src/types.ts +244 -0
  134. package/tsconfig.esm.json +11 -0
  135. package/tsconfig.esm.tsbuildinfo +1 -0
  136. package/tsconfig.json +22 -0
  137. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Navigator for Smart Transcript Directives (ADR-092)
3
+ *
4
+ * Handles NAVIGATE TO directives by pathfinding to target rooms
5
+ * and executing GO commands. Handles random outcomes (e.g., Round Room
6
+ * carousel) by recalculating path when actual location differs from expected.
7
+ */
8
+
9
+ import { NavigateResult } from './types';
10
+
11
+ // WorldModel interface (minimal subset needed for navigation)
12
+ interface WorldModelLike {
13
+ getLocation(entityId: string): string | undefined;
14
+ getEntity(entityId: string): any | undefined;
15
+ findByTrait(traitType: string): any[];
16
+ findWhere(predicate: (entity: any) => boolean): any[];
17
+ findPath(fromRoomId: string, toRoomId: string): string[] | null;
18
+ }
19
+
20
+ // GameEngine interface for command execution
21
+ interface GameEngineLike {
22
+ executeCommand(input: string): Promise<string> | string;
23
+ }
24
+
25
+ // Constants
26
+ const MAX_NAVIGATE_ATTEMPTS = 50; // Max attempts to handle random outcomes
27
+ const ROOM_TRAIT_TYPE = 'if.trait.room';
28
+
29
+ /**
30
+ * Calculate room-to-room distance using BFS
31
+ * Returns Infinity if no path exists
32
+ */
33
+ function calculateRoomDistance(
34
+ world: WorldModelLike,
35
+ fromRoomId: string,
36
+ toRoomId: string
37
+ ): number {
38
+ if (fromRoomId === toRoomId) return 0;
39
+
40
+ const visited = new Set<string>();
41
+ const queue: { roomId: string; distance: number }[] = [{ roomId: fromRoomId, distance: 0 }];
42
+
43
+ while (queue.length > 0) {
44
+ const { roomId, distance } = queue.shift()!;
45
+
46
+ if (visited.has(roomId)) continue;
47
+ visited.add(roomId);
48
+
49
+ const room = world.getEntity(roomId);
50
+ const roomTrait = room?.get?.('room') || room?.getTrait?.('room');
51
+ if (!roomTrait?.exits) continue;
52
+
53
+ for (const [_direction, exitInfo] of Object.entries(roomTrait.exits)) {
54
+ if (!exitInfo) continue;
55
+
56
+ let dest: string | undefined;
57
+ if (typeof exitInfo === 'string') {
58
+ dest = exitInfo;
59
+ } else if (typeof exitInfo === 'object') {
60
+ dest = (exitInfo as any).destination;
61
+ }
62
+
63
+ if (!dest) continue;
64
+
65
+ // Found destination!
66
+ if (dest === toRoomId) {
67
+ return distance + 1;
68
+ }
69
+
70
+ // Add to queue if not visited
71
+ if (!visited.has(dest)) {
72
+ queue.push({ roomId: dest, distance: distance + 1 });
73
+ }
74
+ }
75
+ }
76
+
77
+ return Infinity; // No path found
78
+ }
79
+
80
+ /**
81
+ * Find a room by name (searches identity.name)
82
+ */
83
+ function findRoomByName(world: WorldModelLike, name: string): any | null {
84
+ const nameLower = name.toLowerCase();
85
+
86
+ const rooms = world.findWhere((entity: any) => {
87
+ // Check if it's a room
88
+ const roomTrait = entity.get?.('room') || entity.traits?.get?.('room');
89
+ if (!roomTrait) return false;
90
+
91
+ const identity = entity.get?.('identity') || entity.traits?.get?.('identity');
92
+ if (!identity) return false;
93
+
94
+ const entityName = (identity.name || '').toLowerCase();
95
+ const aliases = (identity.aliases || []).map((a: string) => a.toLowerCase());
96
+
97
+ return entityName === nameLower ||
98
+ entityName.includes(nameLower) ||
99
+ aliases.some((a: string) => a === nameLower || a.includes(nameLower));
100
+ });
101
+
102
+ return rooms[0] || null;
103
+ }
104
+
105
+ /**
106
+ * Get the name of a room by ID
107
+ */
108
+ function getRoomName(world: WorldModelLike, roomId: string): string {
109
+ const room = world.getEntity(roomId);
110
+ if (!room) return roomId;
111
+
112
+ const identity = room.get?.('identity') || room.traits?.get?.('identity');
113
+ return identity?.name || roomId;
114
+ }
115
+
116
+ /**
117
+ * Get the direction that leads from one room to another
118
+ */
119
+ function getDirectionToRoom(
120
+ world: WorldModelLike,
121
+ fromRoomId: string,
122
+ toRoomId: string
123
+ ): string | null {
124
+ const fromRoom = world.getEntity(fromRoomId);
125
+ if (!fromRoom) return null;
126
+
127
+ const roomTrait = fromRoom.get?.('room') || fromRoom.traits?.get?.('room');
128
+ if (!roomTrait || !roomTrait.exits) return null;
129
+
130
+ // Check each exit to find one that leads to the target room
131
+ for (const [direction, exitInfo] of Object.entries(roomTrait.exits)) {
132
+ if (!exitInfo) continue;
133
+
134
+ let destination: string | undefined;
135
+
136
+ if (typeof exitInfo === 'string') {
137
+ destination = exitInfo;
138
+ } else if (typeof exitInfo === 'object') {
139
+ destination = (exitInfo as any).destination;
140
+ }
141
+
142
+ if (destination === toRoomId) {
143
+ return direction.toLowerCase();
144
+ }
145
+ }
146
+
147
+ return null;
148
+ }
149
+
150
+ /**
151
+ * Execute navigation to a target room
152
+ *
153
+ * Uses pathfinding to find route, executes GO commands, and handles
154
+ * random outcomes by recalculating path from actual location.
155
+ */
156
+ export async function executeNavigate(
157
+ targetRoomName: string,
158
+ world: WorldModelLike,
159
+ engine: GameEngineLike,
160
+ playerId: string,
161
+ verbose: boolean = false
162
+ ): Promise<NavigateResult> {
163
+ const path: string[] = [];
164
+ const commands: string[] = [];
165
+
166
+ // Find target room
167
+ if (verbose) {
168
+ console.log(` [NAVIGATE] Searching for room: "${targetRoomName}"`);
169
+ }
170
+ const targetRoom = findRoomByName(world, targetRoomName);
171
+ if (!targetRoom) {
172
+ if (verbose) {
173
+ // Debug: list all rooms to see what's available
174
+ const allRooms = world.findWhere((e: any) => {
175
+ const roomTrait = e.get?.('room') || e.getTrait?.('room');
176
+ return !!roomTrait;
177
+ });
178
+ console.log(` [NAVIGATE] Available rooms: ${allRooms.map((r: any) => {
179
+ const identity = r.get?.('identity') || r.getTrait?.('identity');
180
+ return identity?.name || r.id;
181
+ }).join(', ')}`);
182
+ }
183
+ return {
184
+ success: false,
185
+ path,
186
+ commands,
187
+ error: `Target room "${targetRoomName}" not found`
188
+ };
189
+ }
190
+ if (verbose) {
191
+ console.log(` [NAVIGATE] Found target room: ${targetRoom.id}`);
192
+ }
193
+
194
+ const targetRoomId = targetRoom.id;
195
+
196
+ // Check if already at destination
197
+ let currentRoomId = world.getLocation(playerId);
198
+ if (!currentRoomId) {
199
+ return {
200
+ success: false,
201
+ path,
202
+ commands,
203
+ error: 'Player location unknown'
204
+ };
205
+ }
206
+
207
+ if (currentRoomId === targetRoomId) {
208
+ if (verbose) {
209
+ console.log(` [NAVIGATE] Already at "${targetRoomName}"`);
210
+ }
211
+ return {
212
+ success: true,
213
+ path: [getRoomName(world, currentRoomId)],
214
+ commands
215
+ };
216
+ }
217
+
218
+ // Navigation loop - keep trying until we reach destination or run out of attempts
219
+ let attempts = 0;
220
+
221
+ if (verbose) {
222
+ console.log(` [NAVIGATE] Starting navigation from ${currentRoomId} to ${targetRoomId}`);
223
+ }
224
+
225
+ while (attempts < MAX_NAVIGATE_ATTEMPTS) {
226
+ attempts++;
227
+
228
+ currentRoomId = world.getLocation(playerId);
229
+ if (!currentRoomId) {
230
+ return {
231
+ success: false,
232
+ path,
233
+ commands,
234
+ error: 'Player location unknown'
235
+ };
236
+ }
237
+
238
+ // Already at destination?
239
+ if (currentRoomId === targetRoomId) {
240
+ if (verbose) {
241
+ console.log(` [NAVIGATE] Arrived at "${targetRoomName}" after ${commands.length} commands`);
242
+ }
243
+ return {
244
+ success: true,
245
+ path,
246
+ commands
247
+ };
248
+ }
249
+
250
+ // Find an exit that leads toward the destination
251
+ // We check each exit and verify it leads closer using findPath
252
+ const currentRoom = world.getEntity(currentRoomId);
253
+ const roomTrait = currentRoom?.get?.('room') || currentRoom?.getTrait?.('room');
254
+
255
+ if (!roomTrait?.exits) {
256
+ return {
257
+ success: false,
258
+ path,
259
+ commands,
260
+ error: `No exits from "${getRoomName(world, currentRoomId)}"`
261
+ };
262
+ }
263
+
264
+ // Find the best exit (one that gets us closest to destination)
265
+ let nextRoomId: string | null = null;
266
+ let bestDirection: string | null = null;
267
+ let shortestPathLength = Infinity;
268
+
269
+ for (const [direction, exitInfo] of Object.entries(roomTrait.exits)) {
270
+ if (!exitInfo) continue;
271
+
272
+ let dest: string | undefined;
273
+ if (typeof exitInfo === 'string') {
274
+ dest = exitInfo;
275
+ } else if (typeof exitInfo === 'object') {
276
+ dest = (exitInfo as any).destination;
277
+ }
278
+
279
+ if (!dest) continue;
280
+
281
+ // If this exit leads directly to destination, use it immediately
282
+ if (dest === targetRoomId) {
283
+ nextRoomId = dest;
284
+ bestDirection = direction;
285
+ shortestPathLength = 0;
286
+ break;
287
+ }
288
+
289
+ // Calculate actual room distance from this exit to destination
290
+ const distanceFromDest = calculateRoomDistance(world, dest, targetRoomId);
291
+ if (distanceFromDest < Infinity && distanceFromDest < shortestPathLength) {
292
+ nextRoomId = dest;
293
+ bestDirection = direction;
294
+ shortestPathLength = distanceFromDest;
295
+ }
296
+ }
297
+
298
+ if (!nextRoomId || !bestDirection) {
299
+ return {
300
+ success: false,
301
+ path,
302
+ commands,
303
+ error: `No path from "${getRoomName(world, currentRoomId)}" toward "${targetRoomName}"`
304
+ };
305
+ }
306
+
307
+ // Record current room name
308
+ const currentRoomName = getRoomName(world, currentRoomId);
309
+ if (path.length === 0 || path[path.length - 1] !== currentRoomName) {
310
+ path.push(currentRoomName);
311
+ }
312
+
313
+ // Execute direction command (parser uses just the direction, not "go direction")
314
+ const command = bestDirection.toLowerCase();
315
+ commands.push(command);
316
+
317
+ if (verbose) {
318
+ console.log(` [NAVIGATE] ${currentRoomName} -> go ${bestDirection.toLowerCase()}`);
319
+ }
320
+
321
+ try {
322
+ await engine.executeCommand(command);
323
+ } catch (e) {
324
+ return {
325
+ success: false,
326
+ path,
327
+ commands,
328
+ error: `Command failed: ${command} - ${e instanceof Error ? e.message : String(e)}`
329
+ };
330
+ }
331
+
332
+ // Verify we moved to expected room (or handle random outcome)
333
+ const newRoomId = world.getLocation(playerId);
334
+
335
+ if (newRoomId !== nextRoomId && newRoomId !== currentRoomId) {
336
+ // We moved, but not to expected room (e.g., Round Room carousel)
337
+ // This is OK - we'll recalculate path from wherever we are
338
+ if (verbose) {
339
+ console.log(` [NAVIGATE] Ended up in "${getRoomName(world, newRoomId!)}" (expected "${getRoomName(world, nextRoomId)}")`);
340
+ }
341
+ } else if (newRoomId === currentRoomId) {
342
+ // Didn't move at all - blocked?
343
+ return {
344
+ success: false,
345
+ path,
346
+ commands,
347
+ error: `Blocked: could not go ${bestDirection.toLowerCase()} from "${currentRoomName}"`
348
+ };
349
+ }
350
+
351
+ // Update path with new location
352
+ if (newRoomId) {
353
+ const newRoomName = getRoomName(world, newRoomId);
354
+ path.push(newRoomName);
355
+ }
356
+ }
357
+
358
+ // Exceeded max attempts
359
+ return {
360
+ success: false,
361
+ path,
362
+ commands,
363
+ error: `Navigation exceeded ${MAX_NAVIGATE_ATTEMPTS} attempts (random outcomes?)`
364
+ };
365
+ }