@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,382 @@
1
+ /**
2
+ * Condition Evaluator for Smart Transcript Directives (ADR-092)
3
+ *
4
+ * Evaluates condition expressions against game state.
5
+ *
6
+ * Supported expressions:
7
+ * - location = "Room Name" - Player is in room with that name
8
+ * - room contains "entity" - Entity with that name is in current room
9
+ * - inventory contains "item" - Player has item with that name
10
+ * - not inventory contains "item" - Player does NOT have item
11
+ * - entity "X" exists - Entity with name X exists anywhere
12
+ * - entity "X" alive - NPC entity is not dead
13
+ * - entity "X" in "Room" - Entity X is in specified room
14
+ */
15
+
16
+ import { ConditionResult } from './types';
17
+
18
+ // WorldModel interface (minimal subset needed)
19
+ interface WorldModelLike {
20
+ getLocation(entityId: string): string | null;
21
+ getContents(containerId: string, options?: { includeWorn?: boolean }): any[];
22
+ getEntity(entityId: string): any | null;
23
+ findWhere(predicate: (entity: any) => boolean): any[];
24
+ getAllEntities(): any[];
25
+ }
26
+
27
+ /**
28
+ * Find an entity by name (searches identity.name and aliases)
29
+ * Prioritizes exact matches and actors/NPCs over rooms
30
+ */
31
+ function findEntityByName(world: WorldModelLike, name: string): any | null {
32
+ const nameLower = name.toLowerCase();
33
+
34
+ const allMatches = world.findWhere((entity: any) => {
35
+ const identity = entity.get?.('identity') || entity.traits?.get?.('identity');
36
+ if (!identity) return false;
37
+
38
+ const entityName = (identity.name || '').toLowerCase();
39
+ const aliases = (identity.aliases || []).map((a: string) => a.toLowerCase());
40
+
41
+ return entityName === nameLower ||
42
+ entityName.includes(nameLower) ||
43
+ aliases.some((a: string) => a === nameLower || a.includes(nameLower));
44
+ });
45
+
46
+ if (allMatches.length === 0) return null;
47
+
48
+ // Prioritize: 1) exact name match, 2) exact alias match, 3) actors/NPCs, 4) first match
49
+ const exactNameMatch = allMatches.find((e: any) => {
50
+ const identity = e.get?.('identity') || e.traits?.get?.('identity');
51
+ return (identity?.name || '').toLowerCase() === nameLower;
52
+ });
53
+ if (exactNameMatch) return exactNameMatch;
54
+
55
+ const exactAliasMatch = allMatches.find((e: any) => {
56
+ const identity = e.get?.('identity') || e.traits?.get?.('identity');
57
+ const aliases = (identity?.aliases || []).map((a: string) => a.toLowerCase());
58
+ return aliases.includes(nameLower);
59
+ });
60
+ if (exactAliasMatch) return exactAliasMatch;
61
+
62
+ // Prefer actors/NPCs/combatants over rooms
63
+ const actorMatch = allMatches.find((e: any) =>
64
+ e.get?.('actor') || e.get?.('npc') || e.get?.('combatant') ||
65
+ e.traits?.get?.('actor') || e.traits?.get?.('npc') || e.traits?.get?.('combatant')
66
+ );
67
+ if (actorMatch) return actorMatch;
68
+
69
+ return allMatches[0];
70
+ }
71
+
72
+ /**
73
+ * Find a room by name
74
+ */
75
+ function findRoomByName(world: WorldModelLike, name: string): any | null {
76
+ const nameLower = name.toLowerCase();
77
+
78
+ const rooms = world.findWhere((entity: any) => {
79
+ // Check if it's a room
80
+ const roomTrait = entity.get?.('room') || entity.traits?.get?.('room');
81
+ if (!roomTrait) return false;
82
+
83
+ const identity = entity.get?.('identity') || entity.traits?.get?.('identity');
84
+ if (!identity) return false;
85
+
86
+ const entityName = (identity.name || '').toLowerCase();
87
+ return entityName === nameLower || entityName.includes(nameLower);
88
+ });
89
+
90
+ return rooms[0] || null;
91
+ }
92
+
93
+ /**
94
+ * Get player entity
95
+ */
96
+ function getPlayer(world: WorldModelLike): any | null {
97
+ const players = world.findWhere((entity: any) => {
98
+ const identity = entity.get?.('identity') || entity.traits?.get?.('identity');
99
+ return identity?.name === 'player' || identity?.name === 'yourself';
100
+ });
101
+ return players[0] || null;
102
+ }
103
+
104
+ /**
105
+ * Get the name of a room by ID
106
+ */
107
+ function getRoomName(world: WorldModelLike, roomId: string): string {
108
+ const room = world.getEntity(roomId);
109
+ if (!room) return roomId;
110
+
111
+ const identity = room.get?.('identity') || room.traits?.get?.('identity');
112
+ return identity?.name || roomId;
113
+ }
114
+
115
+ /**
116
+ * Check if an entity is "alive" (for NPCs/Combatants)
117
+ */
118
+ function isEntityAlive(entity: any): boolean {
119
+ // Check for CombatantTrait (used by troll, thief, etc.)
120
+ const combatantTrait = entity.get?.('combatant') || entity.traits?.get?.('combatant');
121
+ if (combatantTrait) {
122
+ // CombatantTrait uses isAlive (not isDead)
123
+ if (combatantTrait.isAlive === false) return false;
124
+ // Check health
125
+ if (combatantTrait.health !== undefined && combatantTrait.health <= 0) return false;
126
+ }
127
+
128
+ // Check for NPC trait with health/alive status
129
+ const npcTrait = entity.get?.('npc') || entity.traits?.get?.('npc');
130
+ if (npcTrait) {
131
+ // Check isDead flag
132
+ if ((entity as any).isDead === true) return false;
133
+ if (npcTrait.isDead === true) return false;
134
+ if (npcTrait.isAlive === false) return false;
135
+ // Check health
136
+ if (npcTrait.health !== undefined && npcTrait.health <= 0) return false;
137
+ }
138
+
139
+ // Default to alive if no NPC/combatant trait or death indicators
140
+ return true;
141
+ }
142
+
143
+ /**
144
+ * Parse and evaluate a condition expression
145
+ */
146
+ export function evaluateCondition(
147
+ condition: string,
148
+ world: WorldModelLike,
149
+ playerId: string
150
+ ): ConditionResult {
151
+ const trimmed = condition.trim();
152
+
153
+ // Handle negation prefix
154
+ const isNegated = trimmed.toLowerCase().startsWith('not ');
155
+ const expr = isNegated ? trimmed.slice(4).trim() : trimmed;
156
+
157
+ // Try each pattern
158
+ let result = tryLocationEquals(expr, world, playerId);
159
+ if (result) return applyNegation(result, isNegated);
160
+
161
+ result = tryRoomContains(expr, world, playerId);
162
+ if (result) return applyNegation(result, isNegated);
163
+
164
+ result = tryInventoryContains(expr, world, playerId);
165
+ if (result) return applyNegation(result, isNegated);
166
+
167
+ result = tryEntityExists(expr, world);
168
+ if (result) return applyNegation(result, isNegated);
169
+
170
+ result = tryEntityAlive(expr, world);
171
+ if (result) return applyNegation(result, isNegated);
172
+
173
+ result = tryEntityInRoom(expr, world);
174
+ if (result) return applyNegation(result, isNegated);
175
+
176
+ // Unknown condition format
177
+ return {
178
+ met: false,
179
+ reason: `Unknown condition format: "${condition}"`
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Apply negation to a result
185
+ */
186
+ function applyNegation(result: ConditionResult, negate: boolean): ConditionResult {
187
+ if (!negate) return result;
188
+ return {
189
+ met: !result.met,
190
+ reason: `NOT (${result.reason})`
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Pattern: location = "Room Name"
196
+ */
197
+ function tryLocationEquals(
198
+ expr: string,
199
+ world: WorldModelLike,
200
+ playerId: string
201
+ ): ConditionResult | null {
202
+ const match = expr.match(/^location\s*=\s*"([^"]+)"$/i);
203
+ if (!match) return null;
204
+
205
+ const targetRoomName = match[1];
206
+ const currentRoomId = world.getLocation(playerId);
207
+
208
+ if (!currentRoomId) {
209
+ return { met: false, reason: `Player location unknown` };
210
+ }
211
+
212
+ const currentRoomName = getRoomName(world, currentRoomId);
213
+ const met = currentRoomName.toLowerCase() === targetRoomName.toLowerCase() ||
214
+ currentRoomName.toLowerCase().includes(targetRoomName.toLowerCase());
215
+
216
+ return {
217
+ met,
218
+ reason: `Player is in "${currentRoomName}" (expected "${targetRoomName}")`
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Pattern: room contains "entity"
224
+ */
225
+ function tryRoomContains(
226
+ expr: string,
227
+ world: WorldModelLike,
228
+ playerId: string
229
+ ): ConditionResult | null {
230
+ const match = expr.match(/^room\s+contains\s+"([^"]+)"$/i);
231
+ if (!match) return null;
232
+
233
+ const entityName = match[1];
234
+ const currentRoomId = world.getLocation(playerId);
235
+
236
+ if (!currentRoomId) {
237
+ return { met: false, reason: `Player location unknown` };
238
+ }
239
+
240
+ // Get room contents
241
+ const contents = world.getContents(currentRoomId);
242
+ const entityNameLower = entityName.toLowerCase();
243
+
244
+ const found = contents.some((item: any) => {
245
+ const identity = item.get?.('identity') || item.traits?.get?.('identity');
246
+ if (!identity) return false;
247
+
248
+ const itemName = (identity.name || '').toLowerCase();
249
+ const aliases = (identity.aliases || []).map((a: string) => a.toLowerCase());
250
+
251
+ return itemName === entityNameLower ||
252
+ itemName.includes(entityNameLower) ||
253
+ aliases.some((a: string) => a === entityNameLower || a.includes(entityNameLower));
254
+ });
255
+
256
+ const roomName = getRoomName(world, currentRoomId);
257
+ return {
258
+ met: found,
259
+ reason: found
260
+ ? `"${entityName}" found in ${roomName}`
261
+ : `"${entityName}" not found in ${roomName}`
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Pattern: inventory contains "item"
267
+ */
268
+ function tryInventoryContains(
269
+ expr: string,
270
+ world: WorldModelLike,
271
+ playerId: string
272
+ ): ConditionResult | null {
273
+ const match = expr.match(/^inventory\s+contains\s+"([^"]+)"$/i);
274
+ if (!match) return null;
275
+
276
+ const itemName = match[1];
277
+
278
+ // Get player inventory
279
+ const inventory = world.getContents(playerId, { includeWorn: true });
280
+ const itemNameLower = itemName.toLowerCase();
281
+
282
+ const found = inventory.some((item: any) => {
283
+ const identity = item.get?.('identity') || item.traits?.get?.('identity');
284
+ if (!identity) return false;
285
+
286
+ const name = (identity.name || '').toLowerCase();
287
+ const aliases = (identity.aliases || []).map((a: string) => a.toLowerCase());
288
+
289
+ return name === itemNameLower ||
290
+ name.includes(itemNameLower) ||
291
+ aliases.some((a: string) => a === itemNameLower || a.includes(itemNameLower));
292
+ });
293
+
294
+ return {
295
+ met: found,
296
+ reason: found
297
+ ? `"${itemName}" found in inventory`
298
+ : `"${itemName}" not in inventory`
299
+ };
300
+ }
301
+
302
+ /**
303
+ * Pattern: entity "X" exists
304
+ */
305
+ function tryEntityExists(
306
+ expr: string,
307
+ world: WorldModelLike
308
+ ): ConditionResult | null {
309
+ const match = expr.match(/^entity\s+"([^"]+)"\s+exists$/i);
310
+ if (!match) return null;
311
+
312
+ const entityName = match[1];
313
+ const entity = findEntityByName(world, entityName);
314
+
315
+ return {
316
+ met: entity !== null,
317
+ reason: entity
318
+ ? `Entity "${entityName}" exists`
319
+ : `Entity "${entityName}" not found`
320
+ };
321
+ }
322
+
323
+ /**
324
+ * Pattern: entity "X" alive
325
+ */
326
+ function tryEntityAlive(
327
+ expr: string,
328
+ world: WorldModelLike
329
+ ): ConditionResult | null {
330
+ const match = expr.match(/^entity\s+"([^"]+)"\s+alive$/i);
331
+ if (!match) return null;
332
+
333
+ const entityName = match[1];
334
+ const entity = findEntityByName(world, entityName);
335
+
336
+ if (!entity) {
337
+ return { met: false, reason: `Entity "${entityName}" not found` };
338
+ }
339
+
340
+ const alive = isEntityAlive(entity);
341
+ return {
342
+ met: alive,
343
+ reason: alive
344
+ ? `Entity "${entityName}" is alive`
345
+ : `Entity "${entityName}" is dead`
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Pattern: entity "X" in "Room"
351
+ */
352
+ function tryEntityInRoom(
353
+ expr: string,
354
+ world: WorldModelLike
355
+ ): ConditionResult | null {
356
+ const match = expr.match(/^entity\s+"([^"]+)"\s+in\s+"([^"]+)"$/i);
357
+ if (!match) return null;
358
+
359
+ const entityName = match[1];
360
+ const roomName = match[2];
361
+
362
+ const entity = findEntityByName(world, entityName);
363
+ if (!entity) {
364
+ return { met: false, reason: `Entity "${entityName}" not found` };
365
+ }
366
+
367
+ const targetRoom = findRoomByName(world, roomName);
368
+ if (!targetRoom) {
369
+ return { met: false, reason: `Room "${roomName}" not found` };
370
+ }
371
+
372
+ const entityLocation = world.getLocation(entity.id);
373
+ const met = entityLocation === targetRoom.id;
374
+
375
+ const actualRoomName = entityLocation ? getRoomName(world, entityLocation) : 'unknown';
376
+ return {
377
+ met,
378
+ reason: met
379
+ ? `"${entityName}" is in "${roomName}"`
380
+ : `"${entityName}" is in "${actualRoomName}", not "${roomName}"`
381
+ };
382
+ }