@sharpee/ext-testing 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 (121) hide show
  1. package/LICENSE +21 -0
  2. package/dist/annotations/context.d.ts +16 -0
  3. package/dist/annotations/context.d.ts.map +1 -0
  4. package/dist/annotations/context.js +42 -0
  5. package/dist/annotations/context.js.map +1 -0
  6. package/dist/annotations/index.d.ts +6 -0
  7. package/dist/annotations/index.d.ts.map +1 -0
  8. package/dist/annotations/index.js +12 -0
  9. package/dist/annotations/index.js.map +1 -0
  10. package/dist/annotations/store.d.ts +11 -0
  11. package/dist/annotations/store.d.ts.map +1 -0
  12. package/dist/annotations/store.js +191 -0
  13. package/dist/annotations/store.js.map +1 -0
  14. package/dist/checkpoints/index.d.ts +3 -0
  15. package/dist/checkpoints/index.d.ts.map +1 -0
  16. package/dist/checkpoints/index.js +12 -0
  17. package/dist/checkpoints/index.js.map +1 -0
  18. package/dist/checkpoints/serializer.d.ts +21 -0
  19. package/dist/checkpoints/serializer.d.ts.map +1 -0
  20. package/dist/checkpoints/serializer.js +95 -0
  21. package/dist/checkpoints/serializer.js.map +1 -0
  22. package/dist/checkpoints/store.d.ts +20 -0
  23. package/dist/checkpoints/store.d.ts.map +1 -0
  24. package/dist/checkpoints/store.js +193 -0
  25. package/dist/checkpoints/store.js.map +1 -0
  26. package/dist/commands/index.d.ts +2 -0
  27. package/dist/commands/index.d.ts.map +1 -0
  28. package/dist/commands/index.js +8 -0
  29. package/dist/commands/index.js.map +1 -0
  30. package/dist/commands/registry.d.ts +28 -0
  31. package/dist/commands/registry.d.ts.map +1 -0
  32. package/dist/commands/registry.js +70 -0
  33. package/dist/commands/registry.js.map +1 -0
  34. package/dist/context/debug-context.d.ts +21 -0
  35. package/dist/context/debug-context.d.ts.map +1 -0
  36. package/dist/context/debug-context.js +172 -0
  37. package/dist/context/debug-context.js.map +1 -0
  38. package/dist/context/index.d.ts +2 -0
  39. package/dist/context/index.d.ts.map +1 -0
  40. package/dist/context/index.js +8 -0
  41. package/dist/context/index.js.map +1 -0
  42. package/dist/extension.d.ts +78 -0
  43. package/dist/extension.d.ts.map +1 -0
  44. package/dist/extension.js +938 -0
  45. package/dist/extension.js.map +1 -0
  46. package/dist/index.d.ts +40 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +63 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/types.d.ts +375 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +8 -0
  53. package/dist/types.js.map +1 -0
  54. package/dist-npm/annotations/context.d.ts +16 -0
  55. package/dist-npm/annotations/context.d.ts.map +1 -0
  56. package/dist-npm/annotations/context.js +42 -0
  57. package/dist-npm/annotations/context.js.map +1 -0
  58. package/dist-npm/annotations/index.d.ts +6 -0
  59. package/dist-npm/annotations/index.d.ts.map +1 -0
  60. package/dist-npm/annotations/index.js +12 -0
  61. package/dist-npm/annotations/index.js.map +1 -0
  62. package/dist-npm/annotations/store.d.ts +11 -0
  63. package/dist-npm/annotations/store.d.ts.map +1 -0
  64. package/dist-npm/annotations/store.js +191 -0
  65. package/dist-npm/annotations/store.js.map +1 -0
  66. package/dist-npm/checkpoints/index.d.ts +3 -0
  67. package/dist-npm/checkpoints/index.d.ts.map +1 -0
  68. package/dist-npm/checkpoints/index.js +12 -0
  69. package/dist-npm/checkpoints/index.js.map +1 -0
  70. package/dist-npm/checkpoints/serializer.d.ts +21 -0
  71. package/dist-npm/checkpoints/serializer.d.ts.map +1 -0
  72. package/dist-npm/checkpoints/serializer.js +95 -0
  73. package/dist-npm/checkpoints/serializer.js.map +1 -0
  74. package/dist-npm/checkpoints/store.d.ts +20 -0
  75. package/dist-npm/checkpoints/store.d.ts.map +1 -0
  76. package/dist-npm/checkpoints/store.js +193 -0
  77. package/dist-npm/checkpoints/store.js.map +1 -0
  78. package/dist-npm/commands/index.d.ts +2 -0
  79. package/dist-npm/commands/index.d.ts.map +1 -0
  80. package/dist-npm/commands/index.js +8 -0
  81. package/dist-npm/commands/index.js.map +1 -0
  82. package/dist-npm/commands/registry.d.ts +28 -0
  83. package/dist-npm/commands/registry.d.ts.map +1 -0
  84. package/dist-npm/commands/registry.js +70 -0
  85. package/dist-npm/commands/registry.js.map +1 -0
  86. package/dist-npm/context/debug-context.d.ts +21 -0
  87. package/dist-npm/context/debug-context.d.ts.map +1 -0
  88. package/dist-npm/context/debug-context.js +172 -0
  89. package/dist-npm/context/debug-context.js.map +1 -0
  90. package/dist-npm/context/index.d.ts +2 -0
  91. package/dist-npm/context/index.d.ts.map +1 -0
  92. package/dist-npm/context/index.js +8 -0
  93. package/dist-npm/context/index.js.map +1 -0
  94. package/dist-npm/extension.d.ts +78 -0
  95. package/dist-npm/extension.d.ts.map +1 -0
  96. package/dist-npm/extension.js +938 -0
  97. package/dist-npm/extension.js.map +1 -0
  98. package/dist-npm/index.d.ts +40 -0
  99. package/dist-npm/index.d.ts.map +1 -0
  100. package/dist-npm/index.js +63 -0
  101. package/dist-npm/index.js.map +1 -0
  102. package/dist-npm/types.d.ts +375 -0
  103. package/dist-npm/types.d.ts.map +1 -0
  104. package/dist-npm/types.js +8 -0
  105. package/dist-npm/types.js.map +1 -0
  106. package/package.json +42 -0
  107. package/src/annotations/context.ts +47 -0
  108. package/src/annotations/index.ts +6 -0
  109. package/src/annotations/store.ts +219 -0
  110. package/src/checkpoints/index.ts +2 -0
  111. package/src/checkpoints/serializer.ts +121 -0
  112. package/src/checkpoints/store.ts +188 -0
  113. package/src/commands/index.ts +1 -0
  114. package/src/commands/registry.ts +81 -0
  115. package/src/context/debug-context.ts +209 -0
  116. package/src/context/index.ts +1 -0
  117. package/src/extension.ts +1089 -0
  118. package/src/index.ts +69 -0
  119. package/src/types.ts +469 -0
  120. package/tsconfig.json +16 -0
  121. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Annotation Context Capture
3
+ *
4
+ * Utility to capture current game state for annotations.
5
+ */
6
+
7
+ import type { WorldModel } from '@sharpee/world-model';
8
+ import type { AnnotationContext } from '../types.js';
9
+
10
+ /**
11
+ * Capture the current game state for annotation context
12
+ */
13
+ export function captureContext(
14
+ world: WorldModel,
15
+ lastCommand: string,
16
+ lastResponse: string
17
+ ): AnnotationContext {
18
+ const player = world.getPlayer();
19
+ const locationId = player ? world.getLocation(player.id) : undefined;
20
+ const location = locationId ? world.getEntity(locationId) : undefined;
21
+ const inventory = player ? world.getContents(player.id) : [];
22
+
23
+ return {
24
+ roomId: locationId ?? 'unknown',
25
+ roomName: location?.name ?? locationId ?? 'unknown',
26
+ turn: (world.getStateValue('turnCount') as number) ?? (world.getStateValue('moves') as number) ?? 0,
27
+ score: (world.getStateValue('score') as number) ?? 0,
28
+ lastCommand,
29
+ lastResponse,
30
+ inventory: inventory.map((e) => e.name ?? e.id),
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Create an empty/default context (for when no command has been executed yet)
36
+ */
37
+ export function createEmptyContext(): AnnotationContext {
38
+ return {
39
+ roomId: 'unknown',
40
+ roomName: 'unknown',
41
+ turn: 0,
42
+ score: 0,
43
+ lastCommand: '',
44
+ lastResponse: '',
45
+ inventory: [],
46
+ };
47
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Annotations module - Playtester feedback capture (ADR-109)
3
+ */
4
+
5
+ export { createAnnotationStore } from './store.js';
6
+ export { captureContext, createEmptyContext } from './context.js';
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Annotation Store Implementation
3
+ *
4
+ * In-memory storage for playtester annotations with session management.
5
+ */
6
+
7
+ import type { AnnotationType, Annotation, AnnotationContext, AnnotationSession, AnnotationStore } from '../types.js';
8
+
9
+ /**
10
+ * Generate a unique ID
11
+ */
12
+ function generateId(): string {
13
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
14
+ }
15
+
16
+ /**
17
+ * Create an in-memory annotation store
18
+ */
19
+ export function createAnnotationStore(): AnnotationStore {
20
+ let currentSession: AnnotationSession | undefined;
21
+ let allAnnotations: Annotation[] = [];
22
+
23
+ return {
24
+ // Session management
25
+ startSession(name: string): string {
26
+ // End any existing session first
27
+ if (currentSession) {
28
+ currentSession.endTime = Date.now();
29
+ }
30
+
31
+ const id = generateId();
32
+ currentSession = {
33
+ id,
34
+ name,
35
+ startTime: Date.now(),
36
+ annotations: [],
37
+ };
38
+
39
+ return id;
40
+ },
41
+
42
+ endSession(): AnnotationSession | undefined {
43
+ if (!currentSession) {
44
+ return undefined;
45
+ }
46
+
47
+ currentSession.endTime = Date.now();
48
+ const ended = currentSession;
49
+ currentSession = undefined;
50
+ return ended;
51
+ },
52
+
53
+ getCurrentSession(): AnnotationSession | undefined {
54
+ return currentSession;
55
+ },
56
+
57
+ // Annotation capture
58
+ addAnnotation(type: AnnotationType, text: string, context: AnnotationContext): Annotation {
59
+ const annotation: Annotation = {
60
+ id: generateId(),
61
+ timestamp: Date.now(),
62
+ type,
63
+ text,
64
+ context,
65
+ sessionId: currentSession?.id,
66
+ };
67
+
68
+ allAnnotations.push(annotation);
69
+
70
+ if (currentSession) {
71
+ currentSession.annotations.push(annotation);
72
+ }
73
+
74
+ return annotation;
75
+ },
76
+
77
+ getAnnotations(): Annotation[] {
78
+ if (currentSession) {
79
+ return [...currentSession.annotations];
80
+ }
81
+ return [...allAnnotations];
82
+ },
83
+
84
+ getAnnotationsByType(type: AnnotationType): Annotation[] {
85
+ const source = currentSession ? currentSession.annotations : allAnnotations;
86
+ return source.filter((a) => a.type === type);
87
+ },
88
+
89
+ // Export
90
+ exportMarkdown(): string {
91
+ return formatMarkdownReport(currentSession, allAnnotations);
92
+ },
93
+
94
+ exportJson(): string {
95
+ const data = currentSession
96
+ ? { session: currentSession }
97
+ : { annotations: allAnnotations };
98
+ return JSON.stringify(data, null, 2);
99
+ },
100
+
101
+ // Cleanup
102
+ clear(): void {
103
+ currentSession = undefined;
104
+ allAnnotations = [];
105
+ },
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Format annotations as a markdown report (per ADR-109)
111
+ */
112
+ function formatMarkdownReport(
113
+ session: AnnotationSession | undefined,
114
+ allAnnotations: Annotation[]
115
+ ): string {
116
+ const annotations = session ? session.annotations : allAnnotations;
117
+ const lines: string[] = [];
118
+
119
+ // Header
120
+ if (session) {
121
+ lines.push(`# Play Test Session: ${session.name}`);
122
+ lines.push(`Date: ${new Date(session.startTime).toLocaleString()}`);
123
+ if (session.endTime) {
124
+ const durationMs = session.endTime - session.startTime;
125
+ const durationMins = Math.round(durationMs / 60000);
126
+ lines.push(`Duration: ${durationMins} minutes`);
127
+ }
128
+ } else {
129
+ lines.push('# Playtest Annotations');
130
+ lines.push(`Generated: ${new Date().toLocaleString()}`);
131
+ }
132
+ lines.push('');
133
+
134
+ // Group by type
135
+ const bugs = annotations.filter((a) => a.type === 'bug');
136
+ const notes = annotations.filter((a) => a.type === 'note');
137
+ const confusing = annotations.filter((a) => a.type === 'confusing');
138
+ const expected = annotations.filter((a) => a.type === 'expected');
139
+ const bookmarks = annotations.filter((a) => a.type === 'bookmark');
140
+ const comments = annotations.filter((a) => a.type === 'comment');
141
+
142
+ // Bugs section
143
+ if (bugs.length > 0) {
144
+ lines.push(`## Bugs (${bugs.length})`);
145
+ bugs.forEach((bug, i) => {
146
+ lines.push(`${i + 1}. [Turn ${bug.context.turn}, ${bug.context.roomName}] "${bug.text}"`);
147
+ lines.push(` - Command: ${bug.context.lastCommand}`);
148
+ lines.push(` - Response: ${truncate(bug.context.lastResponse, 100)}`);
149
+ lines.push('');
150
+ });
151
+ }
152
+
153
+ // Notes section
154
+ if (notes.length > 0) {
155
+ lines.push(`## Notes (${notes.length})`);
156
+ notes.forEach((note, i) => {
157
+ lines.push(`${i + 1}. [Turn ${note.context.turn}, ${note.context.roomName}] "${note.text}"`);
158
+ });
159
+ lines.push('');
160
+ }
161
+
162
+ // Confusion points
163
+ if (confusing.length > 0) {
164
+ lines.push(`## Confusion Points (${confusing.length})`);
165
+ confusing.forEach((c, i) => {
166
+ lines.push(`${i + 1}. [Turn ${c.context.turn}, ${c.context.roomName}]`);
167
+ lines.push(` - After: ${c.context.lastCommand}`);
168
+ lines.push(` - Response: ${truncate(c.context.lastResponse, 100)}`);
169
+ lines.push('');
170
+ });
171
+ }
172
+
173
+ // Expected behavior
174
+ if (expected.length > 0) {
175
+ lines.push(`## Expected Behavior (${expected.length})`);
176
+ expected.forEach((e, i) => {
177
+ lines.push(`${i + 1}. [Turn ${e.context.turn}, ${e.context.roomName}]`);
178
+ lines.push(` - Expected: ${e.text}`);
179
+ lines.push(` - Actual: ${truncate(e.context.lastResponse, 100)}`);
180
+ lines.push('');
181
+ });
182
+ }
183
+
184
+ // Bookmarks
185
+ if (bookmarks.length > 0) {
186
+ lines.push(`## Bookmarks (${bookmarks.length})`);
187
+ bookmarks.forEach((b, i) => {
188
+ lines.push(`${i + 1}. "${b.text}" at Turn ${b.context.turn}, ${b.context.roomName}`);
189
+ });
190
+ lines.push('');
191
+ }
192
+
193
+ // Comments (silent feedback)
194
+ if (comments.length > 0) {
195
+ lines.push(`## Comments (${comments.length})`);
196
+ comments.forEach((c, i) => {
197
+ lines.push(`${i + 1}. [Turn ${c.context.turn}, ${c.context.roomName}] "${c.text}"`);
198
+ });
199
+ lines.push('');
200
+ }
201
+
202
+ // Summary stats
203
+ lines.push('## Summary');
204
+ lines.push(`- Total annotations: ${annotations.length}`);
205
+ lines.push(`- Bugs: ${bugs.length}`);
206
+ lines.push(`- Notes: ${notes.length}`);
207
+ lines.push(`- Confusion points: ${confusing.length}`);
208
+ lines.push(`- Bookmarks: ${bookmarks.length}`);
209
+
210
+ return lines.join('\n');
211
+ }
212
+
213
+ /**
214
+ * Truncate a string to max length
215
+ */
216
+ function truncate(str: string, maxLen: number): string {
217
+ if (str.length <= maxLen) return str;
218
+ return str.substring(0, maxLen - 3) + '...';
219
+ }
@@ -0,0 +1,2 @@
1
+ export { serializeCheckpoint, deserializeCheckpoint, validateCheckpoint } from './serializer.js';
2
+ export { createFileStore, createMemoryStore, createLocalStorageStore } from './store.js';
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Checkpoint Serializer
3
+ *
4
+ * Handles serialization and deserialization of game state for checkpoints.
5
+ * Captures world model state and scheduler state (daemons, fuses).
6
+ */
7
+
8
+ import type { WorldModel } from '@sharpee/world-model';
9
+ import type { CheckpointData, SerializedDaemon, SerializedFuse } from '../types.js';
10
+
11
+ /**
12
+ * Serialize current game state to checkpoint data
13
+ */
14
+ export function serializeCheckpoint(
15
+ world: WorldModel,
16
+ name?: string
17
+ ): CheckpointData {
18
+ const player = world.getPlayer();
19
+ const playerLocation = player ? world.getLocation(player.id) : undefined;
20
+
21
+ // Get world state via WorldModel.toJSON()
22
+ const worldState = world.toJSON();
23
+
24
+ // TODO: Get scheduler state when scheduler API is available
25
+ // For now, scheduler state is omitted
26
+ const schedulerState = undefined;
27
+
28
+ const checkpoint: CheckpointData = {
29
+ version: '1.0.0',
30
+ timestamp: Date.now(),
31
+ metadata: {
32
+ name,
33
+ turn: getTurnNumber(world),
34
+ location: playerLocation,
35
+ },
36
+ worldState,
37
+ schedulerState,
38
+ };
39
+
40
+ return checkpoint;
41
+ }
42
+
43
+ /**
44
+ * Deserialize checkpoint data and restore game state
45
+ */
46
+ export function deserializeCheckpoint(
47
+ checkpoint: CheckpointData,
48
+ world: WorldModel
49
+ ): void {
50
+ // Validate version
51
+ if (checkpoint.version !== '1.0.0') {
52
+ throw new Error(`Unsupported checkpoint version: ${checkpoint.version}`);
53
+ }
54
+
55
+ // Restore world state via WorldModel.loadJSON()
56
+ world.loadJSON(checkpoint.worldState);
57
+
58
+ // TODO: Restore scheduler state when scheduler API is available
59
+ if (checkpoint.schedulerState) {
60
+ restoreSchedulerState(world, checkpoint.schedulerState);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Get current turn number from world
66
+ */
67
+ function getTurnNumber(world: WorldModel): number {
68
+ // Try to get turn from world metadata or state
69
+ // This may need adjustment based on actual WorldModel API
70
+ try {
71
+ const state = JSON.parse(world.toJSON());
72
+ return state.turn ?? 0;
73
+ } catch {
74
+ return 0;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Restore scheduler state (daemons and fuses)
80
+ */
81
+ function restoreSchedulerState(
82
+ world: WorldModel,
83
+ state: {
84
+ turn: number;
85
+ daemons: SerializedDaemon[];
86
+ fuses: SerializedFuse[];
87
+ }
88
+ ): void {
89
+ // TODO: Implement when scheduler serialization API is available
90
+ // This will need access to the engine's scheduler service
91
+ console.warn('Scheduler state restoration not yet implemented');
92
+ }
93
+
94
+ /**
95
+ * Validate checkpoint data structure
96
+ */
97
+ export function validateCheckpoint(data: unknown): data is CheckpointData {
98
+ if (!data || typeof data !== 'object') {
99
+ return false;
100
+ }
101
+
102
+ const checkpoint = data as Partial<CheckpointData>;
103
+
104
+ if (checkpoint.version !== '1.0.0') {
105
+ return false;
106
+ }
107
+
108
+ if (typeof checkpoint.timestamp !== 'number') {
109
+ return false;
110
+ }
111
+
112
+ if (!checkpoint.metadata || typeof checkpoint.metadata !== 'object') {
113
+ return false;
114
+ }
115
+
116
+ if (typeof checkpoint.worldState !== 'string') {
117
+ return false;
118
+ }
119
+
120
+ return true;
121
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Checkpoint Store
3
+ *
4
+ * File-based storage for checkpoints.
5
+ * Supports both Node.js (filesystem) and browser (localStorage) environments.
6
+ */
7
+
8
+ import type { CheckpointData, CheckpointStore } from '../types.js';
9
+ import { validateCheckpoint } from './serializer.js';
10
+
11
+ /**
12
+ * Create a file-based checkpoint store (Node.js)
13
+ */
14
+ export function createFileStore(directory: string): CheckpointStore {
15
+ // Dynamic import for Node.js fs module
16
+ let fs: typeof import('fs') | undefined;
17
+ let path: typeof import('path') | undefined;
18
+
19
+ const ensureFs = async () => {
20
+ if (!fs) {
21
+ fs = await import('fs');
22
+ path = await import('path');
23
+ }
24
+ return { fs, path: path! };
25
+ };
26
+
27
+ const getFilePath = (pathModule: typeof import('path'), name: string): string => {
28
+ return pathModule.join(directory, `${name}.json`);
29
+ };
30
+
31
+ const store: CheckpointStore = {
32
+ async save(name: string, data: CheckpointData): Promise<void> {
33
+ const { fs, path } = await ensureFs();
34
+
35
+ // Ensure directory exists
36
+ if (!fs.existsSync(directory)) {
37
+ fs.mkdirSync(directory, { recursive: true });
38
+ }
39
+
40
+ const filePath = getFilePath(path, name);
41
+ const json = JSON.stringify(data, null, 2);
42
+ fs.writeFileSync(filePath, json, 'utf-8');
43
+ },
44
+
45
+ async load(name: string): Promise<CheckpointData | undefined> {
46
+ const { fs, path } = await ensureFs();
47
+
48
+ const filePath = getFilePath(path, name);
49
+
50
+ if (!fs.existsSync(filePath)) {
51
+ return undefined;
52
+ }
53
+
54
+ try {
55
+ const json = fs.readFileSync(filePath, 'utf-8');
56
+ const data = JSON.parse(json);
57
+
58
+ if (!validateCheckpoint(data)) {
59
+ console.warn(`Invalid checkpoint data in ${filePath}`);
60
+ return undefined;
61
+ }
62
+
63
+ return data;
64
+ } catch (error) {
65
+ console.error(`Failed to load checkpoint ${name}:`, error);
66
+ return undefined;
67
+ }
68
+ },
69
+
70
+ async list(): Promise<string[]> {
71
+ const { fs } = await ensureFs();
72
+
73
+ if (!fs.existsSync(directory)) {
74
+ return [];
75
+ }
76
+
77
+ const files = fs.readdirSync(directory);
78
+ return files
79
+ .filter((f) => f.endsWith('.json'))
80
+ .map((f) => f.replace(/\.json$/, ''));
81
+ },
82
+
83
+ async delete(name: string): Promise<boolean> {
84
+ const { fs, path } = await ensureFs();
85
+
86
+ const filePath = getFilePath(path, name);
87
+
88
+ if (!fs.existsSync(filePath)) {
89
+ return false;
90
+ }
91
+
92
+ fs.unlinkSync(filePath);
93
+ return true;
94
+ },
95
+
96
+ async exists(name: string): Promise<boolean> {
97
+ const { fs, path } = await ensureFs();
98
+
99
+ const filePath = getFilePath(path, name);
100
+ return fs.existsSync(filePath);
101
+ },
102
+ };
103
+
104
+ return store;
105
+ }
106
+
107
+ /**
108
+ * Create a memory-based checkpoint store (for testing or browser)
109
+ */
110
+ export function createMemoryStore(): CheckpointStore {
111
+ const checkpoints = new Map<string, CheckpointData>();
112
+
113
+ return {
114
+ async save(name: string, data: CheckpointData): Promise<void> {
115
+ checkpoints.set(name, data);
116
+ },
117
+
118
+ async load(name: string): Promise<CheckpointData | undefined> {
119
+ return checkpoints.get(name);
120
+ },
121
+
122
+ async list(): Promise<string[]> {
123
+ return Array.from(checkpoints.keys());
124
+ },
125
+
126
+ async delete(name: string): Promise<boolean> {
127
+ return checkpoints.delete(name);
128
+ },
129
+
130
+ async exists(name: string): Promise<boolean> {
131
+ return checkpoints.has(name);
132
+ },
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Create a localStorage-based checkpoint store (browser)
138
+ */
139
+ export function createLocalStorageStore(prefix: string = 'sharpee-checkpoint-'): CheckpointStore {
140
+ const getKey = (name: string) => `${prefix}${name}`;
141
+
142
+ return {
143
+ async save(name: string, data: CheckpointData): Promise<void> {
144
+ const json = JSON.stringify(data);
145
+ localStorage.setItem(getKey(name), json);
146
+ },
147
+
148
+ async load(name: string): Promise<CheckpointData | undefined> {
149
+ const json = localStorage.getItem(getKey(name));
150
+ if (!json) return undefined;
151
+
152
+ try {
153
+ const data = JSON.parse(json);
154
+ if (!validateCheckpoint(data)) {
155
+ console.warn(`Invalid checkpoint data for ${name}`);
156
+ return undefined;
157
+ }
158
+ return data;
159
+ } catch {
160
+ return undefined;
161
+ }
162
+ },
163
+
164
+ async list(): Promise<string[]> {
165
+ const names: string[] = [];
166
+ for (let i = 0; i < localStorage.length; i++) {
167
+ const key = localStorage.key(i);
168
+ if (key?.startsWith(prefix)) {
169
+ names.push(key.slice(prefix.length));
170
+ }
171
+ }
172
+ return names;
173
+ },
174
+
175
+ async delete(name: string): Promise<boolean> {
176
+ const key = getKey(name);
177
+ if (localStorage.getItem(key) === null) {
178
+ return false;
179
+ }
180
+ localStorage.removeItem(key);
181
+ return true;
182
+ },
183
+
184
+ async exists(name: string): Promise<boolean> {
185
+ return localStorage.getItem(getKey(name)) !== null;
186
+ },
187
+ };
188
+ }
@@ -0,0 +1 @@
1
+ export { createCommandRegistry, parseGdtInput, parseTestInput } from './registry.js';
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Command Registry
3
+ *
4
+ * Central registry for debug and test commands.
5
+ * Commands can be looked up by GDT code or test syntax.
6
+ */
7
+
8
+ import type { DebugCommand, CommandCategory, CommandRegistry } from '../types.js';
9
+
10
+ /**
11
+ * Create a new command registry
12
+ */
13
+ export function createCommandRegistry(): CommandRegistry {
14
+ const byCode = new Map<string, DebugCommand>();
15
+ const byTestSyntax = new Map<string, DebugCommand>();
16
+ const allCommands: DebugCommand[] = [];
17
+
18
+ const registry: CommandRegistry = {
19
+ register(command: DebugCommand): void {
20
+ // Register by code (uppercase for case-insensitive lookup)
21
+ byCode.set(command.code.toUpperCase(), command);
22
+
23
+ // Register by test syntax if provided
24
+ if (command.testSyntax) {
25
+ byTestSyntax.set(command.testSyntax.toLowerCase(), command);
26
+ }
27
+
28
+ allCommands.push(command);
29
+ },
30
+
31
+ getByCode(code: string): DebugCommand | undefined {
32
+ return byCode.get(code.toUpperCase());
33
+ },
34
+
35
+ getByTestSyntax(syntax: string): DebugCommand | undefined {
36
+ return byTestSyntax.get(syntax.toLowerCase());
37
+ },
38
+
39
+ getAll(): DebugCommand[] {
40
+ return [...allCommands];
41
+ },
42
+
43
+ getByCategory(category: CommandCategory): DebugCommand[] {
44
+ return allCommands.filter((cmd) => cmd.category === category);
45
+ },
46
+ };
47
+
48
+ return registry;
49
+ }
50
+
51
+ /**
52
+ * Parse a GDT-style command input
53
+ * Format: "CODE arg1 arg2" or just "CODE"
54
+ */
55
+ export function parseGdtInput(input: string): { code: string; args: string[] } {
56
+ const trimmed = input.trim();
57
+ const parts = trimmed.split(/\s+/);
58
+ const code = parts[0] || '';
59
+ const args = parts.slice(1);
60
+
61
+ return { code: code.toUpperCase(), args };
62
+ }
63
+
64
+ /**
65
+ * Parse a test command input
66
+ * Format: "$command arg1 arg2" or just "$command"
67
+ */
68
+ export function parseTestInput(input: string): { syntax: string; args: string[] } | undefined {
69
+ const trimmed = input.trim();
70
+
71
+ if (!trimmed.startsWith('$')) {
72
+ return undefined;
73
+ }
74
+
75
+ const withoutPrefix = trimmed.slice(1);
76
+ const parts = withoutPrefix.split(/\s+/);
77
+ const syntax = parts[0] || '';
78
+ const args = parts.slice(1);
79
+
80
+ return { syntax: syntax.toLowerCase(), args };
81
+ }