@drmxrcy/tcg-lorcana 0.0.0-202602060544

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 (100) hide show
  1. package/README.md +160 -0
  2. package/package.json +45 -0
  3. package/src/__tests__/integration/move-enumeration.test.ts +256 -0
  4. package/src/__tests__/rules/section-01-concepts.test.ts +426 -0
  5. package/src/__tests__/rules/section-03-gameplay.test.ts +298 -0
  6. package/src/__tests__/rules/section-04-turn-structure.test.ts +708 -0
  7. package/src/__tests__/rules/section-05-cards.test.ts +158 -0
  8. package/src/__tests__/rules/section-06-card-types.test.ts +342 -0
  9. package/src/__tests__/rules/section-07-abilities.test.ts +333 -0
  10. package/src/__tests__/rules/section-08-zones.test.ts +231 -0
  11. package/src/__tests__/rules/section-09-damage.test.ts +148 -0
  12. package/src/__tests__/rules/section-10-keywords.test.ts +469 -0
  13. package/src/__tests__/spec-01-foundation-types.test.ts +534 -0
  14. package/src/__tests__/spec-02-zones-card-states.test.ts +295 -0
  15. package/src/card-utils.ts +302 -0
  16. package/src/cards/README.md +296 -0
  17. package/src/cards/abilities/index.ts +175 -0
  18. package/src/cards/index.ts +10 -0
  19. package/src/deck-validation.ts +175 -0
  20. package/src/engine/lorcana-engine.ts +625 -0
  21. package/src/game-definition/__tests__/core-zone-integration.test.ts +553 -0
  22. package/src/game-definition/__tests__/zone-operations.test.ts +362 -0
  23. package/src/game-definition/__tests__/zones.test.ts +176 -0
  24. package/src/game-definition/definition.ts +45 -0
  25. package/src/game-definition/flow/turn-flow.ts +216 -0
  26. package/src/game-definition/index.ts +31 -0
  27. package/src/game-definition/moves/abilities/activate-ability.ts +51 -0
  28. package/src/game-definition/moves/core/__tests__/move-parameter-enumeration.test.ts +316 -0
  29. package/src/game-definition/moves/core/challenge.test.ts +545 -0
  30. package/src/game-definition/moves/core/challenge.ts +81 -0
  31. package/src/game-definition/moves/core/play-card.ts +83 -0
  32. package/src/game-definition/moves/core/quest.test.ts +448 -0
  33. package/src/game-definition/moves/core/quest.ts +49 -0
  34. package/src/game-definition/moves/debug/manual-exert.ts +36 -0
  35. package/src/game-definition/moves/effects/resolve-bag.ts +35 -0
  36. package/src/game-definition/moves/effects/resolve-effect.ts +34 -0
  37. package/src/game-definition/moves/index.ts +85 -0
  38. package/src/game-definition/moves/locations/move-character-to-location.ts +42 -0
  39. package/src/game-definition/moves/resources/put-card-into-inkwell.test.ts +462 -0
  40. package/src/game-definition/moves/resources/put-card-into-inkwell.ts +51 -0
  41. package/src/game-definition/moves/setup/alter-hand.test.ts +395 -0
  42. package/src/game-definition/moves/setup/alter-hand.ts +210 -0
  43. package/src/game-definition/moves/setup/choose-first-player.test.ts +450 -0
  44. package/src/game-definition/moves/setup/choose-first-player.ts +105 -0
  45. package/src/game-definition/moves/setup/draw-cards.ts +37 -0
  46. package/src/game-definition/moves/songs/sing-together.ts +47 -0
  47. package/src/game-definition/moves/songs/sing.ts +56 -0
  48. package/src/game-definition/moves/standard/concede.test.ts +189 -0
  49. package/src/game-definition/moves/standard/concede.ts +72 -0
  50. package/src/game-definition/moves/standard/pass-turn.ts +49 -0
  51. package/src/game-definition/setup/game-setup.ts +19 -0
  52. package/src/game-definition/trackers/tracker-config.ts +23 -0
  53. package/src/game-definition/win-conditions/lore-victory.ts +26 -0
  54. package/src/game-definition/zone-operations.ts +405 -0
  55. package/src/game-definition/zones/zone-configs.ts +59 -0
  56. package/src/game-definition/zones.ts +283 -0
  57. package/src/index.ts +189 -0
  58. package/src/operations/index.ts +7 -0
  59. package/src/operations/lorcana-operations.ts +288 -0
  60. package/src/queries/README.md +56 -0
  61. package/src/resolvers/__tests__/condition-resolver.test.ts +301 -0
  62. package/src/resolvers/condition-registry.ts +70 -0
  63. package/src/resolvers/condition-resolver.ts +85 -0
  64. package/src/resolvers/conditions/basic.ts +81 -0
  65. package/src/resolvers/conditions/card-state.ts +12 -0
  66. package/src/resolvers/conditions/comparison.ts +102 -0
  67. package/src/resolvers/conditions/existence.ts +219 -0
  68. package/src/resolvers/conditions/history.ts +68 -0
  69. package/src/resolvers/conditions/index.ts +15 -0
  70. package/src/resolvers/conditions/logical.ts +55 -0
  71. package/src/resolvers/conditions/resolution.ts +41 -0
  72. package/src/resolvers/conditions/revealed.ts +42 -0
  73. package/src/resolvers/conditions/zone.ts +84 -0
  74. package/src/setup.test.ts +18 -0
  75. package/src/targeting/__tests__/filter-resolver.test.ts +294 -0
  76. package/src/targeting/__tests__/real-cards-targeting.test.ts +303 -0
  77. package/src/targeting/__tests__/targeting-dsl.test.ts +386 -0
  78. package/src/targeting/enum-expansion.ts +387 -0
  79. package/src/targeting/filter-registry.ts +322 -0
  80. package/src/targeting/filter-resolver.ts +145 -0
  81. package/src/targeting/index.ts +91 -0
  82. package/src/targeting/lorcana-target-dsl.ts +495 -0
  83. package/src/targeting/targeting-ui.ts +407 -0
  84. package/src/testing/index.ts +14 -0
  85. package/src/testing/lorcana-test-engine.ts +813 -0
  86. package/src/types/README.md +303 -0
  87. package/src/types/__tests__/lorcana-state.test.ts +168 -0
  88. package/src/types/__tests__/move-enumeration.test.ts +179 -0
  89. package/src/types/branded-types.ts +106 -0
  90. package/src/types/game-state.ts +184 -0
  91. package/src/types/index.ts +87 -0
  92. package/src/types/keywords.ts +187 -0
  93. package/src/types/lorcana-state.ts +260 -0
  94. package/src/types/move-enumeration.ts +126 -0
  95. package/src/types/move-params.ts +216 -0
  96. package/src/validators/index.ts +7 -0
  97. package/src/validators/move-validators.ts +374 -0
  98. package/src/zones/card-state.ts +234 -0
  99. package/src/zones/index.ts +42 -0
  100. package/src/zones/zone-config.ts +150 -0
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # @drmxrcy/tcg-lorcana
2
+
3
+ Disney Lorcana TCG engine built with `@drmxrcy/tcg-core` framework.
4
+
5
+ ## Overview
6
+
7
+ This package implements the complete Disney Lorcana trading card game using the `@drmxrcy/tcg-core` framework. It serves as both a production-ready Lorcana engine and a reference implementation demonstrating best practices for building TCG engines.
8
+
9
+ ## Status
10
+
11
+ 🚧 **Work in Progress** - Package structure and configuration complete. Game logic implementation in progress.
12
+
13
+ ## Installation
14
+
15
+ This is a workspace package. Install dependencies from the monorepo root:
16
+
17
+ ```bash
18
+ bun install
19
+ ```
20
+
21
+ ## Development
22
+
23
+ ### Available Scripts
24
+
25
+ ```bash
26
+ # Type checking
27
+ bun run check-types
28
+
29
+ # Formatting
30
+ bun run format
31
+
32
+ # Linting
33
+ bun run lint
34
+
35
+ # Testing
36
+ bun run test
37
+ bun run test:watch
38
+ bun run test:coverage
39
+
40
+ # Run all checks
41
+ bun run check
42
+ ```
43
+
44
+ ### Project Structure
45
+
46
+ ```
47
+ src/
48
+ ├── game-definition/ # Game definition and configuration
49
+ ├── moves/ # Move handlers
50
+ ├── cards/ # Card definitions and abilities
51
+ ├── types/ # TypeScript type definitions
52
+ ├── queries/ # State query functions
53
+ ├── rules/ # Rule implementations
54
+ └── index.ts # Main entry point
55
+ ```
56
+
57
+ Each directory contains a README.md explaining its purpose and usage.
58
+
59
+ ## Architecture
60
+
61
+ This package integrates with `@drmxrcy/tcg-core` by:
62
+
63
+ 1. **Defining game-specific state** - Extends base `GameState` with Lorcana data
64
+ 2. **Registering moves** - Implements move handlers for all player actions
65
+ 3. **Configuring zones** - Defines zones (Deck, Hand, Play, Discard, Inkwell)
66
+ 4. **Defining flow** - Specifies turn/phase/step structure
67
+ 5. **Implementing abilities** - Creates keyword, triggered, and activated abilities
68
+
69
+ ## Usage Example
70
+
71
+ ```typescript
72
+ import { RuleEngine } from "@drmxrcy/tcg-core";
73
+ import { lorcanaGame } from "@drmxrcy/tcg-lorcana";
74
+
75
+ // Create engine instance
76
+ const engine = new RuleEngine(lorcanaGame, {
77
+ seed: "game-seed-123",
78
+ });
79
+
80
+ // Setup game with player decks
81
+ const initialState = engine.setup({
82
+ players: {
83
+ player1: { deckId: "deck-1" },
84
+ player2: { deckId: "deck-2" },
85
+ },
86
+ });
87
+
88
+ // Execute moves
89
+ const result = engine.executeMove("playCard", {
90
+ playerId: "player1",
91
+ params: { cardId: "card-123" },
92
+ });
93
+
94
+ // Get current state
95
+ const state = engine.getState();
96
+ ```
97
+
98
+ ## Dependencies
99
+
100
+ - `@drmxrcy/tcg-core` - Core TCG engine framework
101
+ - All dependencies from `@drmxrcy/tcg-core` (Immer, Zod, nanoid, seedrandom, etc.)
102
+
103
+ ## Boundaries
104
+
105
+ This package uses Turborepo boundaries to enforce clean architecture:
106
+
107
+ - ✅ Can depend on: `@drmxrcy/tcg-core`
108
+ - ❌ Cannot depend on: Other game engines, UI packages, networking
109
+
110
+ ## Testing
111
+
112
+ All game mechanics are tested through behavior-driven tests:
113
+
114
+ ```typescript
115
+ describe("Quest Move", () => {
116
+ it("gains lore equal to card's lore value", () => {
117
+ const engine = createTestEngine();
118
+ // Test implementation
119
+ });
120
+
121
+ it("exerts the character", () => {
122
+ // Test implementation
123
+ });
124
+
125
+ it("rejects questing with exerted character", () => {
126
+ // Test implementation
127
+ });
128
+ });
129
+ ```
130
+
131
+ ## Documentation
132
+
133
+ - **Product Mission**: `.agent-os/packages/lorcana-engine/product/mission.md`
134
+ - **Tech Stack**: `.agent-os/packages/lorcana-engine/product/tech-stack.md`
135
+ - **Roadmap**: `.agent-os/packages/lorcana-engine/product/roadmap.md`
136
+ - **Integration Guide**: `@packages/core/ENGINE_INTEGRATION.md`
137
+
138
+ ## Contributing
139
+
140
+ This package follows strict coding standards:
141
+
142
+ - TypeScript strict mode (no `any` types)
143
+ - Test-driven development (TDD mandatory)
144
+ - Behavior-driven testing (no mocking)
145
+ - Immutable state (via Immer)
146
+ - Pure functions preferred
147
+ - Biome for formatting and linting
148
+
149
+ See `@.agent-os/standards/` for detailed guidelines.
150
+
151
+ ## License
152
+
153
+ MIT
154
+
155
+ ## References
156
+
157
+ - [Disney Lorcana Official Site](https://www.disneylorcana.com/)
158
+ - [Lorcana Official Rules](https://www.disneylorcana.com/rules)
159
+ - `@drmxrcy/tcg-core` framework documentation
160
+
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@drmxrcy/tcg-lorcana",
3
+ "version": "0.0.0-202602060544",
4
+ "description": "Disney Lorcana TCG engine implementation using @drmxrcy/tcg-core framework",
5
+ "private": false,
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.d.ts",
8
+ "files": [
9
+ "src/"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./src/index.ts",
14
+ "default": "./src/index.ts"
15
+ },
16
+ "./testing": {
17
+ "types": "./src/testing/index.ts",
18
+ "default": "./src/testing/index.ts"
19
+ }
20
+ },
21
+ "scripts": {
22
+ "build": "tsc --noEmit && mkdir -p dist",
23
+ "typecheck": "tsc --noEmit",
24
+ "check-types": "tsc --noEmit",
25
+ "format": "bun x @biomejs/biome check --fix --max-diagnostics=none --diagnostic-level=error --linter-enabled=false ./src",
26
+ "lint": "bun x @biomejs/biome lint --fix --max-diagnostics=none --diagnostic-level=error ./src",
27
+ "test": "AGENT=1 bun test --silent",
28
+ "test:watch": "bun test --watch",
29
+ "test:coverage": "bun test --coverage",
30
+ "check": "turbo format lint check-types test"
31
+ },
32
+ "dependencies": {
33
+ "@drmxrcy/tcg-core": "workspace:*",
34
+ "@drmxrcy/tcg-lorcana-types": "workspace:*",
35
+ "immer": "11.0.1"
36
+ },
37
+ "devDependencies": {
38
+ "@biomejs/biome": "2.3.11",
39
+ "@types/bun": "1.3.4",
40
+ "typescript": "5.9.3"
41
+ },
42
+ "peerDependencies": {
43
+ "typescript": "5.9.3"
44
+ }
45
+ }
@@ -0,0 +1,256 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import {
3
+ LorcanaTestEngine,
4
+ PLAYER_ONE,
5
+ PLAYER_TWO,
6
+ } from "../../testing/lorcana-test-engine";
7
+
8
+ describe("Move Enumeration Integration", () => {
9
+ describe("Phase Transition Integration", () => {
10
+ let testEngine: LorcanaTestEngine;
11
+
12
+ beforeEach(() => {
13
+ testEngine = new LorcanaTestEngine(
14
+ { hand: 7, deck: 10 },
15
+ { hand: 7, deck: 10 },
16
+ { skipPreGame: false },
17
+ );
18
+ });
19
+
20
+ afterEach(() => {
21
+ testEngine.dispose();
22
+ });
23
+
24
+ it.todo("should enumerate moves correctly across setup → mulligan → main phase", () => {
25
+ // Phase 1: Choose First Player
26
+ expect(testEngine.getGamePhase()).toBe("chooseFirstPlayer");
27
+
28
+ const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
29
+ let availableMoves = testEngine.getAvailableMoves(
30
+ choosingPlayer || PLAYER_ONE,
31
+ );
32
+
33
+ // Should have chooseWhoGoesFirstMove available
34
+ expect(availableMoves).toContain("chooseWhoGoesFirstMove");
35
+
36
+ // Execute choice
37
+ testEngine.changeActivePlayer(choosingPlayer || PLAYER_ONE);
38
+ testEngine.chooseWhoGoesFirst(PLAYER_ONE);
39
+
40
+ // Phase 2: Mulligan
41
+ expect(testEngine.getGamePhase()).toBe("mulligan");
42
+
43
+ // Both players should have alterHand available
44
+ availableMoves = testEngine.getAvailableMoves(PLAYER_ONE);
45
+ expect(availableMoves).toContain("alterHand");
46
+
47
+ availableMoves = testEngine.getAvailableMoves(PLAYER_TWO);
48
+ expect(availableMoves).toContain("alterHand");
49
+
50
+ // Execute mulligans
51
+ testEngine.changeActivePlayer(PLAYER_ONE);
52
+ testEngine.alterHand([]);
53
+
54
+ testEngine.changeActivePlayer(PLAYER_TWO);
55
+ testEngine.alterHand([]);
56
+
57
+ // Phase 3: Main Phase (or next phase in flow)
58
+ // After mulligan, game should transition to next phase
59
+ const finalPhase = testEngine.getGamePhase();
60
+ expect(finalPhase).not.toBe("mulligan");
61
+ expect(finalPhase).not.toBe("chooseFirstPlayer");
62
+ });
63
+
64
+ it("should show different available moves for different players", () => {
65
+ const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
66
+ const otherPlayer =
67
+ choosingPlayer === PLAYER_ONE ? PLAYER_TWO : PLAYER_ONE;
68
+
69
+ // Choosing player should see chooseWhoGoesFirstMove
70
+ const chooserMoves = testEngine.getAvailableMoves(
71
+ choosingPlayer || PLAYER_ONE,
72
+ );
73
+ expect(chooserMoves).toContain("chooseWhoGoesFirstMove");
74
+
75
+ // Other player should see no moves
76
+ const otherMoves = testEngine.getAvailableMoves(otherPlayer);
77
+ expect(otherMoves).toEqual([]);
78
+ });
79
+
80
+ it("should enumerate moves correctly after OTP transitions", () => {
81
+ const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
82
+
83
+ // Before choosing - only chooser has moves
84
+ const chooserMoves = testEngine.getAvailableMoves(
85
+ choosingPlayer || PLAYER_ONE,
86
+ );
87
+ expect(chooserMoves.length).toBeGreaterThan(0);
88
+
89
+ // Choose first player
90
+ testEngine.changeActivePlayer(choosingPlayer || PLAYER_ONE);
91
+ testEngine.chooseWhoGoesFirst(PLAYER_ONE);
92
+
93
+ // After choosing - OTP (player_one) should have mulligan move (has priority)
94
+ const p1Moves = testEngine.getAvailableMoves(PLAYER_ONE);
95
+ const p2Moves = testEngine.getAvailableMoves(PLAYER_TWO);
96
+
97
+ expect(p1Moves).toContain("alterHand");
98
+ // Player two doesn't have priority yet, so no alterHand
99
+ expect(p2Moves).not.toContain("alterHand");
100
+ });
101
+ });
102
+
103
+ describe("Multiple Players", () => {
104
+ let testEngine: LorcanaTestEngine;
105
+
106
+ beforeEach(() => {
107
+ testEngine = new LorcanaTestEngine(
108
+ { hand: 7, deck: 10 },
109
+ { hand: 7, deck: 10 },
110
+ { skipPreGame: false },
111
+ );
112
+ });
113
+
114
+ afterEach(() => {
115
+ testEngine.dispose();
116
+ });
117
+
118
+ it("should enumerate different moves for each player simultaneously", () => {
119
+ const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
120
+ const otherPlayer =
121
+ choosingPlayer === PLAYER_ONE ? PLAYER_TWO : PLAYER_ONE;
122
+
123
+ // Get moves for both players at the same time
124
+ const chooserMoves = testEngine.getAvailableMoves(
125
+ choosingPlayer || PLAYER_ONE,
126
+ );
127
+ const otherPlayerMoves = testEngine.getAvailableMoves(otherPlayer);
128
+
129
+ // Chooser has moves, other doesn't
130
+ expect(chooserMoves.length).toBeGreaterThan(0);
131
+ expect(otherPlayerMoves.length).toBe(0);
132
+
133
+ // Results should be independent
134
+ expect(chooserMoves).toContain("chooseWhoGoesFirstMove");
135
+ expect(otherPlayerMoves).not.toContain("chooseWhoGoesFirstMove");
136
+ });
137
+
138
+ it("should handle rapid enumeration calls without errors", () => {
139
+ const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
140
+
141
+ // Call enumeration multiple times rapidly
142
+ for (let i = 0; i < 10; i++) {
143
+ const moves = testEngine.getAvailableMoves(
144
+ choosingPlayer || PLAYER_ONE,
145
+ );
146
+ expect(moves).toContain("chooseWhoGoesFirstMove");
147
+ }
148
+
149
+ // State should remain consistent
150
+ expect(testEngine.getGamePhase()).toBe("chooseFirstPlayer");
151
+ });
152
+ });
153
+
154
+ describe("Edge Cases", () => {
155
+ let testEngine: LorcanaTestEngine;
156
+
157
+ beforeEach(() => {
158
+ testEngine = new LorcanaTestEngine(
159
+ { hand: 7, deck: 10 },
160
+ { hand: 7, deck: 10 },
161
+ { skipPreGame: false },
162
+ );
163
+ });
164
+
165
+ afterEach(() => {
166
+ testEngine.dispose();
167
+ });
168
+
169
+ it("should handle enumeration for player with no available moves", () => {
170
+ const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
171
+ const otherPlayer =
172
+ choosingPlayer === PLAYER_ONE ? PLAYER_TWO : PLAYER_ONE;
173
+
174
+ // Other player has no moves during chooseFirstPlayer phase
175
+ const moves = testEngine.getAvailableMoves(otherPlayer);
176
+
177
+ expect(moves).toEqual([]);
178
+ expect(Array.isArray(moves)).toBe(true);
179
+ });
180
+
181
+ it("should handle enumeration after move is no longer available", () => {
182
+ const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
183
+
184
+ // Before executing move
185
+ let moves = testEngine.getAvailableMoves(choosingPlayer || PLAYER_ONE);
186
+ expect(moves).toContain("chooseWhoGoesFirstMove");
187
+
188
+ // Execute move
189
+ testEngine.changeActivePlayer(choosingPlayer || PLAYER_ONE);
190
+ testEngine.chooseWhoGoesFirst(PLAYER_ONE);
191
+
192
+ // After executing move - should no longer be available
193
+ moves = testEngine.getAvailableMoves(choosingPlayer || PLAYER_ONE);
194
+ expect(moves).not.toContain("chooseWhoGoesFirstMove");
195
+ });
196
+
197
+ it("should return consistent results for same state", () => {
198
+ const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
199
+
200
+ // Call multiple times without changing state
201
+ const moves1 = testEngine.getAvailableMoves(choosingPlayer || PLAYER_ONE);
202
+ const moves2 = testEngine.getAvailableMoves(choosingPlayer || PLAYER_ONE);
203
+ const moves3 = testEngine.getAvailableMoves(choosingPlayer || PLAYER_ONE);
204
+
205
+ // All results should be identical
206
+ expect(moves1).toEqual(moves2);
207
+ expect(moves2).toEqual(moves3);
208
+ });
209
+
210
+ it("should handle invalid player ID gracefully", () => {
211
+ // Try to get moves for a non-existent player
212
+ const moves = testEngine.getAvailableMoves("invalid_player_id");
213
+
214
+ // Should return empty array (no moves available)
215
+ expect(Array.isArray(moves)).toBe(true);
216
+ expect(moves).toEqual([]);
217
+ });
218
+ });
219
+
220
+ describe("Performance", () => {
221
+ let testEngine: LorcanaTestEngine;
222
+
223
+ beforeEach(() => {
224
+ testEngine = new LorcanaTestEngine(
225
+ { hand: 7, deck: 10 },
226
+ { hand: 7, deck: 10 },
227
+ { skipPreGame: false },
228
+ );
229
+ });
230
+
231
+ afterEach(() => {
232
+ testEngine.dispose();
233
+ });
234
+
235
+ it("should enumerate moves quickly (<100ms for typical state)", () => {
236
+ const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
237
+
238
+ const startTime = performance.now();
239
+
240
+ // Enumerate moves 100 times
241
+ for (let i = 0; i < 100; i++) {
242
+ testEngine.getAvailableMoves(choosingPlayer || PLAYER_ONE);
243
+ }
244
+
245
+ const endTime = performance.now();
246
+ const totalTime = endTime - startTime;
247
+ const avgTime = totalTime / 100;
248
+
249
+ // Average time per enumeration should be under 500ms (higher threshold for CI parallel execution)
250
+ expect(avgTime).toBeLessThan(500);
251
+
252
+ // Log for visibility (not an assertion)
253
+ console.log(`Average enumeration time: ${avgTime.toFixed(2)}ms`);
254
+ });
255
+ });
256
+ });