@willwade/aac-processors 0.0.11 → 0.0.13
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.
- package/README.md +44 -41
- package/dist/cli/index.js +7 -0
- package/dist/core/analyze.js +1 -0
- package/dist/core/treeStructure.d.ts +45 -2
- package/dist/core/treeStructure.js +22 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +20 -3
- package/dist/{analytics → optional/analytics}/history.d.ts +15 -4
- package/dist/{analytics → optional/analytics}/history.js +3 -3
- package/dist/optional/analytics/index.d.ts +30 -0
- package/dist/optional/analytics/index.js +78 -0
- package/dist/optional/analytics/metrics/comparison.d.ts +36 -0
- package/dist/optional/analytics/metrics/comparison.js +334 -0
- package/dist/optional/analytics/metrics/core.d.ts +45 -0
- package/dist/optional/analytics/metrics/core.js +575 -0
- package/dist/optional/analytics/metrics/effort.d.ts +147 -0
- package/dist/optional/analytics/metrics/effort.js +211 -0
- package/dist/optional/analytics/metrics/index.d.ts +15 -0
- package/dist/optional/analytics/metrics/index.js +36 -0
- package/dist/optional/analytics/metrics/obl-types.d.ts +93 -0
- package/dist/optional/analytics/metrics/obl-types.js +7 -0
- package/dist/optional/analytics/metrics/obl.d.ts +40 -0
- package/dist/optional/analytics/metrics/obl.js +287 -0
- package/dist/optional/analytics/metrics/sentence.d.ts +49 -0
- package/dist/optional/analytics/metrics/sentence.js +112 -0
- package/dist/optional/analytics/metrics/types.d.ts +157 -0
- package/dist/optional/analytics/metrics/types.js +7 -0
- package/dist/optional/analytics/metrics/vocabulary.d.ts +65 -0
- package/dist/optional/analytics/metrics/vocabulary.js +142 -0
- package/dist/optional/analytics/reference/index.d.ts +51 -0
- package/dist/optional/analytics/reference/index.js +102 -0
- package/dist/optional/analytics/utils/idGenerator.d.ts +59 -0
- package/dist/optional/analytics/utils/idGenerator.js +96 -0
- package/dist/optional/symbolTools.js +13 -16
- package/dist/processors/astericsGridProcessor.d.ts +15 -0
- package/dist/processors/astericsGridProcessor.js +17 -0
- package/dist/processors/gridset/helpers.d.ts +4 -1
- package/dist/processors/gridset/helpers.js +4 -0
- package/dist/processors/gridset/pluginTypes.js +51 -50
- package/dist/processors/gridset/symbolExtractor.js +3 -2
- package/dist/processors/gridset/symbolSearch.js +9 -7
- package/dist/processors/gridsetProcessor.js +82 -20
- package/dist/processors/index.d.ts +1 -0
- package/dist/processors/index.js +5 -3
- package/dist/processors/obfProcessor.js +37 -2
- package/dist/processors/obfsetProcessor.d.ts +26 -0
- package/dist/processors/obfsetProcessor.js +179 -0
- package/dist/processors/snap/helpers.d.ts +5 -1
- package/dist/processors/snap/helpers.js +5 -0
- package/dist/processors/snapProcessor.d.ts +2 -0
- package/dist/processors/snapProcessor.js +184 -5
- package/dist/processors/touchchatProcessor.js +50 -4
- package/dist/types/aac.d.ts +67 -0
- package/dist/types/aac.js +33 -0
- package/dist/validation/gridsetValidator.js +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Comparative Analysis
|
|
4
|
+
*
|
|
5
|
+
* Compares two AAC board sets to identify missing/extra words,
|
|
6
|
+
* analyze vocabulary differences, and generate CARE component scores.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.ComparisonAnalyzer = void 0;
|
|
10
|
+
const sentence_1 = require("./sentence");
|
|
11
|
+
const vocabulary_1 = require("./vocabulary");
|
|
12
|
+
const index_1 = require("../reference/index");
|
|
13
|
+
class ComparisonAnalyzer {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.vocabAnalyzer = new vocabulary_1.VocabularyAnalyzer();
|
|
16
|
+
this.sentenceAnalyzer = new sentence_1.SentenceAnalyzer();
|
|
17
|
+
this.referenceLoader = new index_1.ReferenceLoader();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Compare two board sets
|
|
21
|
+
*/
|
|
22
|
+
compare(targetResult, compareResult, options) {
|
|
23
|
+
// Create base result from target
|
|
24
|
+
const baseResult = { ...targetResult };
|
|
25
|
+
// Create word maps
|
|
26
|
+
const targetWords = new Map();
|
|
27
|
+
targetResult.buttons.forEach((btn) => {
|
|
28
|
+
const existing = targetWords.get(btn.label);
|
|
29
|
+
if (!existing || btn.effort < existing.effort) {
|
|
30
|
+
targetWords.set(btn.label, btn);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const compareWords = new Map();
|
|
34
|
+
compareResult.buttons.forEach((btn) => {
|
|
35
|
+
const existing = compareWords.get(btn.label);
|
|
36
|
+
if (!existing || btn.effort < existing.effort) {
|
|
37
|
+
compareWords.set(btn.label, btn);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
// Find missing/extra/overlapping words
|
|
41
|
+
const missingWords = [];
|
|
42
|
+
const extraWords = [];
|
|
43
|
+
const overlappingWords = [];
|
|
44
|
+
// Words in comparison but not in target
|
|
45
|
+
compareWords.forEach((btn, label) => {
|
|
46
|
+
if (!targetWords.has(label)) {
|
|
47
|
+
missingWords.push(label);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
overlappingWords.push(label);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
// Words in target but not in comparison
|
|
54
|
+
targetWords.forEach((btn, label) => {
|
|
55
|
+
if (!compareWords.has(label)) {
|
|
56
|
+
extraWords.push(label);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
// Sort alphabetically
|
|
60
|
+
missingWords.sort((a, b) => a.localeCompare(b));
|
|
61
|
+
extraWords.sort((a, b) => a.localeCompare(b));
|
|
62
|
+
overlappingWords.sort((a, b) => a.localeCompare(b));
|
|
63
|
+
// Add comparison metrics to buttons
|
|
64
|
+
const enrichedButtons = targetResult.buttons.map((btn) => {
|
|
65
|
+
const compBtn = compareWords.get(btn.label);
|
|
66
|
+
return {
|
|
67
|
+
...btn,
|
|
68
|
+
comp_level: compBtn?.level,
|
|
69
|
+
comp_effort: compBtn?.effort,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
// Calculate CARE components
|
|
73
|
+
const careComponents = this.calculateCareComponents(targetResult, compareResult, overlappingWords);
|
|
74
|
+
// Analyze high/low effort words
|
|
75
|
+
const highEffortWords = [];
|
|
76
|
+
const lowEffortWords = [];
|
|
77
|
+
targetWords.forEach((btn, label) => {
|
|
78
|
+
const compBtn = compareWords.get(label);
|
|
79
|
+
const compEffort = compBtn?.effort || 0;
|
|
80
|
+
// Word is harder in target than comparison
|
|
81
|
+
if (compEffort > 0 && btn.effort > compEffort * 1.5) {
|
|
82
|
+
highEffortWords.push(label);
|
|
83
|
+
}
|
|
84
|
+
// Word is easier in target than comparison
|
|
85
|
+
else if (compEffort > 0 && btn.effort < compEffort * 0.67) {
|
|
86
|
+
lowEffortWords.push(label);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
highEffortWords.sort((a, b) => {
|
|
90
|
+
const targetBtnA = targetWords.get(a);
|
|
91
|
+
const targetBtnB = targetWords.get(b);
|
|
92
|
+
const diffA = (targetBtnA?.effort || 0) - (compareWords.get(a)?.effort || 0);
|
|
93
|
+
const diffB = (targetBtnB?.effort || 0) - (compareWords.get(b)?.effort || 0);
|
|
94
|
+
return diffB - diffA;
|
|
95
|
+
});
|
|
96
|
+
lowEffortWords.sort((a, b) => {
|
|
97
|
+
const targetBtnA = targetWords.get(a);
|
|
98
|
+
const targetBtnB = targetWords.get(b);
|
|
99
|
+
const diffA = (compareWords.get(a)?.effort || 0) - (targetBtnA?.effort || 0);
|
|
100
|
+
const diffB = (compareWords.get(b)?.effort || 0) - (targetBtnB?.effort || 0);
|
|
101
|
+
return diffB - diffA;
|
|
102
|
+
});
|
|
103
|
+
// Sentence analysis
|
|
104
|
+
let sentences = [];
|
|
105
|
+
if (options?.includeSentences) {
|
|
106
|
+
const testSentences = this.referenceLoader.loadSentences();
|
|
107
|
+
const targetSentences = this.sentenceAnalyzer.analyzeSentences(targetResult, testSentences);
|
|
108
|
+
const compareSentences = this.sentenceAnalyzer.analyzeSentences(compareResult, testSentences);
|
|
109
|
+
sentences = targetSentences.map((ts, idx) => ({
|
|
110
|
+
sentence: ts.sentence,
|
|
111
|
+
words: ts.words,
|
|
112
|
+
effort: ts.effort,
|
|
113
|
+
typing: ts.typing,
|
|
114
|
+
comp_effort: compareSentences[idx]?.effort || 0,
|
|
115
|
+
comp_typing: compareSentences[idx]?.typing || false,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
// Core vocabulary analysis
|
|
119
|
+
const coreLists = this.referenceLoader.loadCoreLists();
|
|
120
|
+
const cores = {};
|
|
121
|
+
coreLists.forEach((list) => {
|
|
122
|
+
let targetTotal = 0;
|
|
123
|
+
let compareTotal = 0;
|
|
124
|
+
let targetCovered = 0;
|
|
125
|
+
let compareCovered = 0;
|
|
126
|
+
list.words.forEach((word) => {
|
|
127
|
+
const targetBtn = targetWords.get(word);
|
|
128
|
+
const compareBtn = compareWords.get(word);
|
|
129
|
+
if (targetBtn) {
|
|
130
|
+
targetCovered++;
|
|
131
|
+
targetTotal += targetBtn.effort;
|
|
132
|
+
}
|
|
133
|
+
if (compareBtn) {
|
|
134
|
+
compareCovered++;
|
|
135
|
+
compareTotal += compareBtn.effort;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
cores[list.id] = {
|
|
139
|
+
name: list.name,
|
|
140
|
+
list: list.words,
|
|
141
|
+
average_effort: targetCovered > 0 ? targetTotal / targetCovered : 0,
|
|
142
|
+
comp_effort: compareCovered > 0 ? compareTotal / compareCovered : 0,
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
// Analyze missing from specific lists
|
|
146
|
+
const missing = {};
|
|
147
|
+
coreLists.forEach((list) => {
|
|
148
|
+
const listMissing = [];
|
|
149
|
+
list.words.forEach((word) => {
|
|
150
|
+
if (!targetWords.has(word)) {
|
|
151
|
+
listMissing.push(word);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
if (listMissing.length > 0) {
|
|
155
|
+
missing[list.id] = {
|
|
156
|
+
name: list.name,
|
|
157
|
+
list: listMissing,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
// Fringe vocabulary analysis
|
|
162
|
+
const fringeWords = this.analyzeFringe(targetWords, compareWords);
|
|
163
|
+
const commonFringeWords = this.analyzeCommonFringe(targetWords, compareWords);
|
|
164
|
+
return {
|
|
165
|
+
...baseResult,
|
|
166
|
+
buttons: enrichedButtons,
|
|
167
|
+
// Target set metrics
|
|
168
|
+
target_effort_score: this.calculateEffortScore(targetResult),
|
|
169
|
+
// Comparison set metrics
|
|
170
|
+
comp_boards: compareResult.total_boards,
|
|
171
|
+
comp_buttons: compareResult.total_buttons,
|
|
172
|
+
comp_words: compareResult.total_words,
|
|
173
|
+
comp_grid: compareResult.grid,
|
|
174
|
+
comp_effort_score: this.calculateEffortScore(compareResult),
|
|
175
|
+
// Vocabulary comparison
|
|
176
|
+
missing_words: missingWords,
|
|
177
|
+
extra_words: extraWords,
|
|
178
|
+
overlapping_words: overlappingWords,
|
|
179
|
+
// Missing from lists
|
|
180
|
+
missing,
|
|
181
|
+
// High/low effort words
|
|
182
|
+
high_effort_words: highEffortWords.slice(0, 100),
|
|
183
|
+
low_effort_words: lowEffortWords.slice(0, 100),
|
|
184
|
+
// Core analysis
|
|
185
|
+
cores,
|
|
186
|
+
// CARE components
|
|
187
|
+
care_components: careComponents,
|
|
188
|
+
// Sentences
|
|
189
|
+
sentences,
|
|
190
|
+
// Fringe
|
|
191
|
+
fringe_words: fringeWords,
|
|
192
|
+
common_fringe_words: commonFringeWords,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Calculate CARE component scores
|
|
197
|
+
*/
|
|
198
|
+
calculateCareComponents(targetResult, compareResult, _overlappingWords) {
|
|
199
|
+
// Create word maps
|
|
200
|
+
const targetWords = new Map();
|
|
201
|
+
targetResult.buttons.forEach((btn) => {
|
|
202
|
+
const existing = targetWords.get(btn.label);
|
|
203
|
+
if (!existing || btn.effort < existing.effort) {
|
|
204
|
+
targetWords.set(btn.label, btn);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
const compareWords = new Map();
|
|
208
|
+
compareResult.buttons.forEach((btn) => {
|
|
209
|
+
const existing = compareWords.get(btn.label);
|
|
210
|
+
if (!existing || btn.effort < existing.effort) {
|
|
211
|
+
compareWords.set(btn.label, btn);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
// Load reference data
|
|
215
|
+
const coreLists = this.referenceLoader.loadCoreLists();
|
|
216
|
+
const fringe = this.referenceLoader.loadFringe();
|
|
217
|
+
const sentences = this.referenceLoader.loadSentences();
|
|
218
|
+
// Calculate core coverage
|
|
219
|
+
let coreCount = 0;
|
|
220
|
+
let compCoreCount = 0;
|
|
221
|
+
const allCoreWords = new Set();
|
|
222
|
+
coreLists.forEach((list) => {
|
|
223
|
+
list.words.forEach((word) => allCoreWords.add(word.toLowerCase()));
|
|
224
|
+
});
|
|
225
|
+
allCoreWords.forEach((word) => {
|
|
226
|
+
if (targetWords.has(word))
|
|
227
|
+
coreCount++;
|
|
228
|
+
if (compareWords.has(word))
|
|
229
|
+
compCoreCount++;
|
|
230
|
+
});
|
|
231
|
+
// Calculate sentence construction effort
|
|
232
|
+
let sentenceEffort = 0;
|
|
233
|
+
let compSentenceEffort = 0;
|
|
234
|
+
let sentenceWordCount = 0;
|
|
235
|
+
sentences.forEach((words) => {
|
|
236
|
+
words.forEach((word) => {
|
|
237
|
+
const targetBtn = targetWords.get(word);
|
|
238
|
+
const compareBtn = compareWords.get(word);
|
|
239
|
+
if (targetBtn) {
|
|
240
|
+
sentenceEffort += targetBtn.effort;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
sentenceEffort += 10 + word.length * 2.5; // Spelling effort
|
|
244
|
+
}
|
|
245
|
+
if (compareBtn) {
|
|
246
|
+
compSentenceEffort += compareBtn.effort;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
compSentenceEffort += 10 + word.length * 2.5;
|
|
250
|
+
}
|
|
251
|
+
sentenceWordCount++;
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
const avgSentenceEffort = sentenceWordCount > 0 ? sentenceEffort / sentenceWordCount : 0;
|
|
255
|
+
const compAvgSentenceEffort = sentenceWordCount > 0 ? compSentenceEffort / sentenceWordCount : 0;
|
|
256
|
+
// Calculate fringe coverage
|
|
257
|
+
let fringeCount = 0;
|
|
258
|
+
let compFringeCount = 0;
|
|
259
|
+
let commonFringeCount = 0;
|
|
260
|
+
fringe.forEach((word) => {
|
|
261
|
+
const inTarget = targetWords.has(word);
|
|
262
|
+
const inCompare = compareWords.has(word);
|
|
263
|
+
if (inTarget)
|
|
264
|
+
fringeCount++;
|
|
265
|
+
if (inCompare)
|
|
266
|
+
compFringeCount++;
|
|
267
|
+
if (inTarget && inCompare)
|
|
268
|
+
commonFringeCount++;
|
|
269
|
+
});
|
|
270
|
+
return {
|
|
271
|
+
core: coreCount,
|
|
272
|
+
comp_core: compCoreCount,
|
|
273
|
+
sentences: avgSentenceEffort,
|
|
274
|
+
comp_sentences: compAvgSentenceEffort,
|
|
275
|
+
fringe: fringeCount,
|
|
276
|
+
comp_fringe: compFringeCount,
|
|
277
|
+
common_fringe: commonFringeCount,
|
|
278
|
+
comp_common_fringe: commonFringeCount,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Analyze fringe vocabulary
|
|
283
|
+
*/
|
|
284
|
+
analyzeFringe(targetWords, compareWords) {
|
|
285
|
+
const fringe = this.referenceLoader.loadFringe();
|
|
286
|
+
const result = [];
|
|
287
|
+
fringe.forEach((word) => {
|
|
288
|
+
const targetBtn = targetWords.get(word);
|
|
289
|
+
const compareBtn = compareWords.get(word);
|
|
290
|
+
if (targetBtn) {
|
|
291
|
+
result.push({
|
|
292
|
+
word,
|
|
293
|
+
effort: targetBtn.effort,
|
|
294
|
+
comp_effort: compareBtn?.effort || 0,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
result.sort((a, b) => a.effort - b.effort);
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Analyze common fringe vocabulary
|
|
303
|
+
*/
|
|
304
|
+
analyzeCommonFringe(targetWords, compareWords) {
|
|
305
|
+
const fringe = this.referenceLoader.loadFringe();
|
|
306
|
+
const result = [];
|
|
307
|
+
fringe.forEach((word) => {
|
|
308
|
+
const targetBtn = targetWords.get(word);
|
|
309
|
+
const compareBtn = compareWords.get(word);
|
|
310
|
+
if (targetBtn && compareBtn) {
|
|
311
|
+
result.push({
|
|
312
|
+
word,
|
|
313
|
+
effort: targetBtn.effort,
|
|
314
|
+
comp_effort: compareBtn.effort,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
result.sort((a, b) => a.effort - b.effort);
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Calculate overall effort score for a metrics result
|
|
323
|
+
*/
|
|
324
|
+
calculateEffortScore(result) {
|
|
325
|
+
if (result.buttons.length === 0)
|
|
326
|
+
return 0;
|
|
327
|
+
let totalEffort = 0;
|
|
328
|
+
result.buttons.forEach((btn) => {
|
|
329
|
+
totalEffort += btn.effort;
|
|
330
|
+
});
|
|
331
|
+
return totalEffort / result.buttons.length;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
exports.ComparisonAnalyzer = ComparisonAnalyzer;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Metrics Analysis Engine
|
|
3
|
+
*
|
|
4
|
+
* Implements the main BFS traversal algorithm from the Ruby aac-metrics tool.
|
|
5
|
+
* Calculates effort scores for all buttons in an AAC board set.
|
|
6
|
+
*
|
|
7
|
+
* Based on: aac-metrics/lib/aac-metrics/metrics.rb
|
|
8
|
+
*/
|
|
9
|
+
import { AACTree } from '../../../core/treeStructure';
|
|
10
|
+
import { MetricsResult } from './types';
|
|
11
|
+
export declare class MetricsCalculator {
|
|
12
|
+
private locale;
|
|
13
|
+
/**
|
|
14
|
+
* Main analysis function - calculates metrics for an AAC tree
|
|
15
|
+
*
|
|
16
|
+
* @param tree - The AAC tree to analyze
|
|
17
|
+
* @returns Complete metrics result
|
|
18
|
+
*/
|
|
19
|
+
analyze(tree: AACTree): MetricsResult;
|
|
20
|
+
/**
|
|
21
|
+
* Build reference maps for semantic_id and clone_id frequencies
|
|
22
|
+
*/
|
|
23
|
+
private buildReferenceMaps;
|
|
24
|
+
/**
|
|
25
|
+
* Count scan items for visual scanning effort
|
|
26
|
+
* When block scanning is enabled, count unique scan blocks instead of individual buttons
|
|
27
|
+
*/
|
|
28
|
+
private countScanItems;
|
|
29
|
+
/**
|
|
30
|
+
* Analyze starting from a specific board
|
|
31
|
+
*/
|
|
32
|
+
private analyzeFrom;
|
|
33
|
+
/**
|
|
34
|
+
* Calculate what percentage of links to this board match semantic_id/clone_id
|
|
35
|
+
*/
|
|
36
|
+
private calculateBoardLinkPercentages;
|
|
37
|
+
/**
|
|
38
|
+
* Calculate grid dimensions from the tree
|
|
39
|
+
*/
|
|
40
|
+
private calculateGridDimensions;
|
|
41
|
+
/**
|
|
42
|
+
* Calculate scanning steps and selections for a button based on access method
|
|
43
|
+
*/
|
|
44
|
+
private calculateScanSteps;
|
|
45
|
+
}
|