@willwade/aac-processors 0.1.7 → 0.1.8

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 (32) hide show
  1. package/dist/analytics.d.ts +7 -0
  2. package/dist/analytics.js +23 -0
  3. package/dist/browser/index.browser.js +5 -0
  4. package/dist/browser/metrics.js +17 -0
  5. package/dist/browser/processors/gridset/helpers.js +390 -0
  6. package/dist/browser/processors/snap/helpers.js +252 -0
  7. package/dist/browser/utilities/analytics/history.js +116 -0
  8. package/dist/browser/utilities/analytics/metrics/comparison.js +477 -0
  9. package/dist/browser/utilities/analytics/metrics/core.js +775 -0
  10. package/dist/browser/utilities/analytics/metrics/effort.js +221 -0
  11. package/dist/browser/utilities/analytics/metrics/obl-types.js +6 -0
  12. package/dist/browser/utilities/analytics/metrics/obl.js +282 -0
  13. package/dist/browser/utilities/analytics/metrics/sentence.js +121 -0
  14. package/dist/browser/utilities/analytics/metrics/types.js +6 -0
  15. package/dist/browser/utilities/analytics/metrics/vocabulary.js +138 -0
  16. package/dist/browser/utilities/analytics/reference/browser.js +67 -0
  17. package/dist/browser/utilities/analytics/reference/index.js +129 -0
  18. package/dist/browser/utils/dotnetTicks.js +17 -0
  19. package/dist/index.browser.d.ts +1 -0
  20. package/dist/index.browser.js +18 -1
  21. package/dist/index.node.d.ts +2 -2
  22. package/dist/index.node.js +5 -5
  23. package/dist/metrics.d.ts +17 -0
  24. package/dist/metrics.js +44 -0
  25. package/dist/utilities/analytics/metrics/comparison.d.ts +2 -1
  26. package/dist/utilities/analytics/metrics/comparison.js +3 -3
  27. package/dist/utilities/analytics/metrics/vocabulary.d.ts +2 -2
  28. package/dist/utilities/analytics/reference/browser.d.ts +31 -0
  29. package/dist/utilities/analytics/reference/browser.js +73 -0
  30. package/dist/utilities/analytics/reference/index.d.ts +21 -0
  31. package/dist/utilities/analytics/reference/index.js +22 -46
  32. package/package.json +9 -1
@@ -0,0 +1,252 @@
1
+ import { AACSemanticCategory, AACSemanticIntent } from '../../core/treeStructure';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import Database from 'better-sqlite3';
5
+ import { dotNetTicksToDate } from '../../utils/dotnetTicks';
6
+ // Minimal Snap helpers (stubs) to align with processors/<engine>/helpers pattern
7
+ // NOTE: Snap buttons currently do not populate resolvedImageEntry; these helpers
8
+ // therefore return empty collections until image resolution is implemented.
9
+ function collectFiles(root, matcher, maxDepth = 3) {
10
+ const results = new Set();
11
+ const stack = [{ dir: root, depth: 0 }];
12
+ while (stack.length > 0) {
13
+ const current = stack.pop();
14
+ if (!current)
15
+ continue;
16
+ if (current.depth > maxDepth)
17
+ continue;
18
+ let entries;
19
+ try {
20
+ entries = fs.readdirSync(current.dir, { withFileTypes: true });
21
+ }
22
+ catch (error) {
23
+ continue;
24
+ }
25
+ for (const entry of entries) {
26
+ const fullPath = path.join(current.dir, entry.name);
27
+ if (entry.isDirectory()) {
28
+ stack.push({ dir: fullPath, depth: current.depth + 1 });
29
+ }
30
+ else if (matcher(fullPath)) {
31
+ results.add(fullPath);
32
+ }
33
+ }
34
+ }
35
+ return Array.from(results);
36
+ }
37
+ /**
38
+ * Build a map of button IDs to resolved image entries for a specific page.
39
+ * Mirrors the Grid helper for consumers that expect image reference data.
40
+ */
41
+ export function getPageTokenImageMap(tree, pageId) {
42
+ const map = new Map();
43
+ const page = tree.getPage(pageId);
44
+ if (!page)
45
+ return map;
46
+ for (const btn of page.buttons) {
47
+ if (btn.resolvedImageEntry)
48
+ map.set(btn.id, String(btn.resolvedImageEntry));
49
+ }
50
+ return map;
51
+ }
52
+ /**
53
+ * Collect all image entry paths referenced in a Snap tree.
54
+ * Currently empty until resolvedImageEntry is populated by the processor.
55
+ */
56
+ export function getAllowedImageEntries(_tree) {
57
+ return new Set();
58
+ }
59
+ /**
60
+ * Read a binary asset from a Snap pageset.
61
+ * Not implemented yet; provided for API symmetry with other processors.
62
+ */
63
+ export function openImage(_dbOrFile, _entryPath) {
64
+ return null;
65
+ }
66
+ /**
67
+ * Find Tobii Communicator Snap package paths
68
+ * Searches in %LOCALAPPDATA%\Packages for Snap-related packages
69
+ * @param packageNamePattern Optional pattern to filter package names (default: 'TobiiDynavox')
70
+ * @returns Array of Snap package path information
71
+ */
72
+ export function findSnapPackages(packageNamePattern = 'TobiiDynavox') {
73
+ const results = [];
74
+ // Only works on Windows
75
+ if (process.platform !== 'win32') {
76
+ return results;
77
+ }
78
+ try {
79
+ const localAppData = process.env.LOCALAPPDATA;
80
+ if (!localAppData) {
81
+ return results;
82
+ }
83
+ const packagesPath = path.join(localAppData, 'Packages');
84
+ // Check if Packages directory exists
85
+ if (!fs.existsSync(packagesPath)) {
86
+ return results;
87
+ }
88
+ // Enumerate packages
89
+ const packages = fs.readdirSync(packagesPath, { withFileTypes: true });
90
+ for (const packageDir of packages) {
91
+ if (!packageDir.isDirectory())
92
+ continue;
93
+ const packageName = packageDir.name;
94
+ // Filter by pattern
95
+ if (packageName.includes(packageNamePattern)) {
96
+ results.push({
97
+ packageName,
98
+ packagePath: path.join(packagesPath, packageName),
99
+ });
100
+ }
101
+ }
102
+ }
103
+ catch (error) {
104
+ // Silently fail if directory access fails
105
+ }
106
+ return results;
107
+ }
108
+ /**
109
+ * Find the first Snap package path matching the pattern
110
+ * Convenience method for when you expect only one Snap installation
111
+ * @param packageNamePattern Optional pattern to filter package names (default: 'TobiiDynavox')
112
+ * @returns Path to the first matching Snap package, or null if not found
113
+ */
114
+ export function findSnapPackagePath(packageNamePattern = 'TobiiDynavox') {
115
+ const packages = findSnapPackages(packageNamePattern);
116
+ return packages.length > 0 ? packages[0].packagePath : null;
117
+ }
118
+ /**
119
+ * Find Snap user directories and their vocab files (.sps/.spb)
120
+ * Typical path:
121
+ * C:\Users\{username}\AppData\Roaming\Tobii Dynavox\Snap Scene\Users\{userId}\
122
+ * @param packageNamePattern Optional package filter (default TobiiDynavox)
123
+ * @returns Array of user info with vocab paths
124
+ */
125
+ export function findSnapUsers(packageNamePattern = 'TobiiDynavox') {
126
+ const results = [];
127
+ if (process.platform !== 'win32') {
128
+ return results;
129
+ }
130
+ const packagePath = findSnapPackagePath(packageNamePattern);
131
+ if (!packagePath) {
132
+ return results;
133
+ }
134
+ const usersRoot = path.join(packagePath, 'LocalState', 'Users');
135
+ if (!fs.existsSync(usersRoot)) {
136
+ return results;
137
+ }
138
+ const entries = fs.readdirSync(usersRoot, { withFileTypes: true });
139
+ for (const entry of entries) {
140
+ if (!entry.isDirectory())
141
+ continue;
142
+ if (entry.name.toLowerCase().startsWith('swiftkey'))
143
+ continue;
144
+ const userPath = path.join(usersRoot, entry.name);
145
+ const vocabPaths = collectFiles(userPath, (full) => {
146
+ const ext = path.extname(full).toLowerCase();
147
+ return ext === '.sps' || ext === '.spb';
148
+ }, 2);
149
+ results.push({
150
+ userId: entry.name,
151
+ userPath,
152
+ vocabPaths,
153
+ });
154
+ }
155
+ return results;
156
+ }
157
+ /**
158
+ * Find vocab files for a specific Snap user (or all users)
159
+ * @param userId Optional user identifier filter (case-sensitive directory name)
160
+ * @param packageNamePattern Optional package filter
161
+ * @returns Array of vocab file paths
162
+ */
163
+ export function findSnapUserVocabularies(userId, packageNamePattern = 'TobiiDynavox') {
164
+ const users = findSnapUsers(packageNamePattern).filter((u) => !userId || u.userId === userId);
165
+ return users.flatMap((u) => u.vocabPaths);
166
+ }
167
+ /**
168
+ * Attempt to find history/analytics files for a Snap user by name
169
+ * Currently searches for files containing "history" under the user directory
170
+ * @param userId User identifier (directory name)
171
+ * @param packageNamePattern Optional package filter
172
+ * @returns Array of history file paths (may be empty if not found)
173
+ */
174
+ export function findSnapUserHistory(userId, packageNamePattern = 'TobiiDynavox') {
175
+ const user = findSnapUsers(packageNamePattern).find((u) => u.userId === userId);
176
+ if (!user)
177
+ return [];
178
+ return collectFiles(user.userPath, (full) => path.basename(full).toLowerCase().includes('history'), 2);
179
+ }
180
+ /**
181
+ * Check whether TD Snap appears to be installed (Windows only)
182
+ */
183
+ export function isSnapInstalled(packageNamePattern = 'TobiiDynavox') {
184
+ if (process.platform !== 'win32')
185
+ return false;
186
+ return Boolean(findSnapPackagePath(packageNamePattern));
187
+ }
188
+ /**
189
+ * Read Snap usage history from a pageset file (.sps/.spb)
190
+ */
191
+ export function readSnapUsage(pagesetPath) {
192
+ if (!fs.existsSync(pagesetPath))
193
+ return [];
194
+ const db = new Database(pagesetPath, { readonly: true });
195
+ const tableCheck = db
196
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('ButtonUsage','Button')")
197
+ .all();
198
+ if (tableCheck.length < 2)
199
+ return [];
200
+ const rows = db
201
+ .prepare(`
202
+ SELECT
203
+ bu.ButtonUniqueId as ButtonId,
204
+ bu.Timestamp as TickValue,
205
+ bu.Modeling as Modeling,
206
+ bu.AccessMethod as AccessMethod,
207
+ b.Label as Label,
208
+ b.Message as Message
209
+ FROM ButtonUsage bu
210
+ LEFT JOIN Button b ON bu.ButtonUniqueId = b.UniqueId
211
+ WHERE bu.Timestamp IS NOT NULL
212
+ ORDER BY bu.Timestamp ASC
213
+ `)
214
+ .all();
215
+ const events = new Map();
216
+ for (const row of rows) {
217
+ const buttonId = row.ButtonId ?? 'unknown';
218
+ const label = row.Label ?? undefined;
219
+ const message = row.Message ?? undefined;
220
+ const content = message || label || '';
221
+ const entry = events.get(buttonId) ??
222
+ {
223
+ id: `snap:${buttonId}`,
224
+ content,
225
+ occurrences: [],
226
+ platform: {
227
+ label,
228
+ message,
229
+ buttonId,
230
+ },
231
+ };
232
+ entry.occurrences.push({
233
+ timestamp: dotNetTicksToDate(BigInt(row.TickValue ?? 0)),
234
+ modeling: row.Modeling === 1,
235
+ accessMethod: row.AccessMethod ?? null,
236
+ type: 'button',
237
+ buttonId: row.ButtonId,
238
+ intent: AACSemanticIntent.SPEAK_TEXT,
239
+ category: AACSemanticCategory.COMMUNICATION,
240
+ });
241
+ events.set(buttonId, entry);
242
+ }
243
+ return Array.from(events.values());
244
+ }
245
+ /**
246
+ * Read Snap usage history for a user (all pagesets)
247
+ */
248
+ export function readSnapUsageForUser(userId, packageNamePattern = 'TobiiDynavox') {
249
+ const users = findSnapUsers(packageNamePattern).filter((u) => !userId || u.userId === userId);
250
+ const pagesets = users.flatMap((u) => u.vocabPaths);
251
+ return pagesets.flatMap((p) => readSnapUsage(p));
252
+ }
@@ -0,0 +1,116 @@
1
+ import { dotNetTicksToDate } from '../../utils/dotnetTicks';
2
+ import { findGrid3Users, readAllGrid3History as readAllGrid3HistoryImpl, readGrid3History as readGrid3HistoryImpl, readGrid3HistoryForUser as readGrid3HistoryForUserImpl, } from '../../processors/gridset/helpers';
3
+ import { findSnapUsers, readSnapUsage as readSnapUsageImpl, readSnapUsageForUser as readSnapUsageForUserImpl, } from '../../processors/snap/helpers';
4
+ export { dotNetTicksToDate };
5
+ const generateUuid = () => {
6
+ if (typeof globalThis.crypto?.randomUUID === 'function') {
7
+ return globalThis.crypto.randomUUID();
8
+ }
9
+ // RFC4122-ish fallback for Node without crypto.randomUUID
10
+ const hex = '0123456789abcdef';
11
+ const bytes = Array.from({ length: 16 }, () => Math.floor(Math.random() * 256));
12
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
13
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
14
+ const toHex = (b) => hex[(b >> 4) & 0x0f] + hex[b & 0x0f];
15
+ return (toHex(bytes[0]) +
16
+ toHex(bytes[1]) +
17
+ toHex(bytes[2]) +
18
+ toHex(bytes[3]) +
19
+ '-' +
20
+ toHex(bytes[4]) +
21
+ toHex(bytes[5]) +
22
+ '-' +
23
+ toHex(bytes[6]) +
24
+ toHex(bytes[7]) +
25
+ '-' +
26
+ toHex(bytes[8]) +
27
+ toHex(bytes[9]) +
28
+ '-' +
29
+ toHex(bytes[10]) +
30
+ toHex(bytes[11]) +
31
+ toHex(bytes[12]) +
32
+ toHex(bytes[13]) +
33
+ toHex(bytes[14]) +
34
+ toHex(bytes[15]));
35
+ };
36
+ export function exportHistoryToBaton(entries, options) {
37
+ const exportDate = options?.exportDate instanceof Date
38
+ ? options.exportDate.toISOString()
39
+ : options?.exportDate || new Date().toISOString();
40
+ const anonymousUUID = options?.anonymousUUID || generateUuid();
41
+ const sentences = entries.map((entry) => ({
42
+ uuid: generateUuid(),
43
+ anonymousUUID,
44
+ content: entry.content,
45
+ metadata: entry.occurrences.map((occ) => ({
46
+ timestamp: occ.timestamp.toISOString(),
47
+ latitude: occ.latitude ?? null,
48
+ longitude: occ.longitude ?? null,
49
+ })),
50
+ source: entry.source,
51
+ }));
52
+ return {
53
+ version: options?.version || '1.0',
54
+ exportDate,
55
+ encryption: options?.encryption || 'none',
56
+ sentenceCount: sentences.length,
57
+ sentences,
58
+ };
59
+ }
60
+ /**
61
+ * Read Grid 3 phrase history from a history.sqlite database and tag entries with their source.
62
+ */
63
+ export function readGrid3History(historyDbPath) {
64
+ return readGrid3HistoryImpl(historyDbPath).map((e) => ({
65
+ ...e,
66
+ source: 'Grid',
67
+ }));
68
+ }
69
+ /**
70
+ * Read Grid 3 history for a specific user/language combination.
71
+ */
72
+ export function readGrid3HistoryForUser(userName, langCode) {
73
+ return readGrid3HistoryForUserImpl(userName, langCode).map((e) => ({
74
+ ...e,
75
+ source: 'Grid',
76
+ }));
77
+ }
78
+ /**
79
+ * Read every available Grid 3 history database on the machine.
80
+ */
81
+ export function readAllGrid3History() {
82
+ return readAllGrid3HistoryImpl().map((e) => ({ ...e, source: 'Grid' }));
83
+ }
84
+ /**
85
+ * Read Snap button usage from a pageset database and tag entries with source.
86
+ */
87
+ export function readSnapUsage(pagesetPath) {
88
+ return readSnapUsageImpl(pagesetPath).map((e) => ({ ...e, source: 'Snap' }));
89
+ }
90
+ /**
91
+ * Read Snap usage for a specific user across all discovered pagesets.
92
+ */
93
+ export function readSnapUsageForUser(userId, packageNamePattern = 'TobiiDynavox') {
94
+ return readSnapUsageForUserImpl(userId, packageNamePattern).map((e) => ({
95
+ ...e,
96
+ source: 'Snap',
97
+ }));
98
+ }
99
+ export function listSnapUsers() {
100
+ return findSnapUsers();
101
+ }
102
+ /**
103
+ * List Grid 3 users on the current machine.
104
+ */
105
+ export function listGrid3Users() {
106
+ return findGrid3Users();
107
+ }
108
+ /**
109
+ * Convenience helper to gather all available history across Grid 3 and Snap.
110
+ * Returns an empty array if no history files are present.
111
+ */
112
+ export function collectUnifiedHistory() {
113
+ const gridHistory = readAllGrid3History();
114
+ const snapHistory = findSnapUsers().flatMap((u) => readSnapUsageForUser(u.userId));
115
+ return [...gridHistory, ...snapHistory];
116
+ }