@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.
Files changed (56) hide show
  1. package/README.md +44 -41
  2. package/dist/cli/index.js +7 -0
  3. package/dist/core/analyze.js +1 -0
  4. package/dist/core/treeStructure.d.ts +45 -2
  5. package/dist/core/treeStructure.js +22 -3
  6. package/dist/index.d.ts +2 -1
  7. package/dist/index.js +20 -3
  8. package/dist/{analytics → optional/analytics}/history.d.ts +15 -4
  9. package/dist/{analytics → optional/analytics}/history.js +3 -3
  10. package/dist/optional/analytics/index.d.ts +30 -0
  11. package/dist/optional/analytics/index.js +78 -0
  12. package/dist/optional/analytics/metrics/comparison.d.ts +36 -0
  13. package/dist/optional/analytics/metrics/comparison.js +334 -0
  14. package/dist/optional/analytics/metrics/core.d.ts +45 -0
  15. package/dist/optional/analytics/metrics/core.js +575 -0
  16. package/dist/optional/analytics/metrics/effort.d.ts +147 -0
  17. package/dist/optional/analytics/metrics/effort.js +211 -0
  18. package/dist/optional/analytics/metrics/index.d.ts +15 -0
  19. package/dist/optional/analytics/metrics/index.js +36 -0
  20. package/dist/optional/analytics/metrics/obl-types.d.ts +93 -0
  21. package/dist/optional/analytics/metrics/obl-types.js +7 -0
  22. package/dist/optional/analytics/metrics/obl.d.ts +40 -0
  23. package/dist/optional/analytics/metrics/obl.js +287 -0
  24. package/dist/optional/analytics/metrics/sentence.d.ts +49 -0
  25. package/dist/optional/analytics/metrics/sentence.js +112 -0
  26. package/dist/optional/analytics/metrics/types.d.ts +157 -0
  27. package/dist/optional/analytics/metrics/types.js +7 -0
  28. package/dist/optional/analytics/metrics/vocabulary.d.ts +65 -0
  29. package/dist/optional/analytics/metrics/vocabulary.js +142 -0
  30. package/dist/optional/analytics/reference/index.d.ts +51 -0
  31. package/dist/optional/analytics/reference/index.js +102 -0
  32. package/dist/optional/analytics/utils/idGenerator.d.ts +59 -0
  33. package/dist/optional/analytics/utils/idGenerator.js +96 -0
  34. package/dist/optional/symbolTools.js +13 -16
  35. package/dist/processors/astericsGridProcessor.d.ts +15 -0
  36. package/dist/processors/astericsGridProcessor.js +17 -0
  37. package/dist/processors/gridset/helpers.d.ts +4 -1
  38. package/dist/processors/gridset/helpers.js +4 -0
  39. package/dist/processors/gridset/pluginTypes.js +51 -50
  40. package/dist/processors/gridset/symbolExtractor.js +3 -2
  41. package/dist/processors/gridset/symbolSearch.js +9 -7
  42. package/dist/processors/gridsetProcessor.js +82 -20
  43. package/dist/processors/index.d.ts +1 -0
  44. package/dist/processors/index.js +5 -3
  45. package/dist/processors/obfProcessor.js +37 -2
  46. package/dist/processors/obfsetProcessor.d.ts +26 -0
  47. package/dist/processors/obfsetProcessor.js +179 -0
  48. package/dist/processors/snap/helpers.d.ts +5 -1
  49. package/dist/processors/snap/helpers.js +5 -0
  50. package/dist/processors/snapProcessor.d.ts +2 -0
  51. package/dist/processors/snapProcessor.js +184 -5
  52. package/dist/processors/touchchatProcessor.js +50 -4
  53. package/dist/types/aac.d.ts +67 -0
  54. package/dist/types/aac.js +33 -0
  55. package/dist/validation/gridsetValidator.js +10 -0
  56. 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
+ }