@willwade/aac-processors 0.0.11 → 0.0.12

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 (38) hide show
  1. package/dist/cli/index.js +7 -0
  2. package/dist/core/analyze.js +1 -0
  3. package/dist/core/treeStructure.d.ts +12 -2
  4. package/dist/core/treeStructure.js +6 -2
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.js +20 -3
  7. package/dist/{analytics → optional/analytics}/history.d.ts +3 -3
  8. package/dist/{analytics → optional/analytics}/history.js +3 -3
  9. package/dist/optional/analytics/index.d.ts +28 -0
  10. package/dist/optional/analytics/index.js +73 -0
  11. package/dist/optional/analytics/metrics/comparison.d.ts +36 -0
  12. package/dist/optional/analytics/metrics/comparison.js +330 -0
  13. package/dist/optional/analytics/metrics/core.d.ts +36 -0
  14. package/dist/optional/analytics/metrics/core.js +422 -0
  15. package/dist/optional/analytics/metrics/effort.d.ts +137 -0
  16. package/dist/optional/analytics/metrics/effort.js +198 -0
  17. package/dist/optional/analytics/metrics/index.d.ts +15 -0
  18. package/dist/optional/analytics/metrics/index.js +36 -0
  19. package/dist/optional/analytics/metrics/sentence.d.ts +49 -0
  20. package/dist/optional/analytics/metrics/sentence.js +112 -0
  21. package/dist/optional/analytics/metrics/types.d.ts +157 -0
  22. package/dist/optional/analytics/metrics/types.js +7 -0
  23. package/dist/optional/analytics/metrics/vocabulary.d.ts +65 -0
  24. package/dist/optional/analytics/metrics/vocabulary.js +140 -0
  25. package/dist/optional/analytics/reference/index.d.ts +51 -0
  26. package/dist/optional/analytics/reference/index.js +102 -0
  27. package/dist/optional/analytics/utils/idGenerator.d.ts +59 -0
  28. package/dist/optional/analytics/utils/idGenerator.js +96 -0
  29. package/dist/processors/gridsetProcessor.js +25 -0
  30. package/dist/processors/index.d.ts +1 -0
  31. package/dist/processors/index.js +5 -3
  32. package/dist/processors/obfProcessor.js +25 -2
  33. package/dist/processors/obfsetProcessor.d.ts +26 -0
  34. package/dist/processors/obfsetProcessor.js +179 -0
  35. package/dist/processors/snapProcessor.js +29 -1
  36. package/dist/processors/touchchatProcessor.js +27 -0
  37. package/dist/types/aac.d.ts +4 -0
  38. package/package.json +1 -1
@@ -0,0 +1,330 @@
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 diffA = targetWords.get(a).effort - (compareWords.get(a)?.effort || 0);
91
+ const diffB = targetWords.get(b).effort - (compareWords.get(b)?.effort || 0);
92
+ return diffB - diffA;
93
+ });
94
+ lowEffortWords.sort((a, b) => {
95
+ const diffA = (compareWords.get(a)?.effort || 0) - targetWords.get(a).effort;
96
+ const diffB = (compareWords.get(b)?.effort || 0) - targetWords.get(b).effort;
97
+ return diffB - diffA;
98
+ });
99
+ // Sentence analysis
100
+ let sentences = [];
101
+ if (options?.includeSentences) {
102
+ const testSentences = this.referenceLoader.loadSentences();
103
+ const targetSentences = this.sentenceAnalyzer.analyzeSentences(targetResult, testSentences);
104
+ const compareSentences = this.sentenceAnalyzer.analyzeSentences(compareResult, testSentences);
105
+ sentences = targetSentences.map((ts, idx) => ({
106
+ sentence: ts.sentence,
107
+ words: ts.words,
108
+ effort: ts.effort,
109
+ typing: ts.typing,
110
+ comp_effort: compareSentences[idx]?.effort || 0,
111
+ comp_typing: compareSentences[idx]?.typing || false,
112
+ }));
113
+ }
114
+ // Core vocabulary analysis
115
+ const coreLists = this.referenceLoader.loadCoreLists();
116
+ const cores = {};
117
+ coreLists.forEach((list) => {
118
+ let targetTotal = 0;
119
+ let compareTotal = 0;
120
+ let targetCovered = 0;
121
+ let compareCovered = 0;
122
+ list.words.forEach((word) => {
123
+ const targetBtn = targetWords.get(word);
124
+ const compareBtn = compareWords.get(word);
125
+ if (targetBtn) {
126
+ targetCovered++;
127
+ targetTotal += targetBtn.effort;
128
+ }
129
+ if (compareBtn) {
130
+ compareCovered++;
131
+ compareTotal += compareBtn.effort;
132
+ }
133
+ });
134
+ cores[list.id] = {
135
+ name: list.name,
136
+ list: list.words,
137
+ average_effort: targetCovered > 0 ? targetTotal / targetCovered : 0,
138
+ comp_effort: compareCovered > 0 ? compareTotal / compareCovered : 0,
139
+ };
140
+ });
141
+ // Analyze missing from specific lists
142
+ const missing = {};
143
+ coreLists.forEach((list) => {
144
+ const listMissing = [];
145
+ list.words.forEach((word) => {
146
+ if (!targetWords.has(word)) {
147
+ listMissing.push(word);
148
+ }
149
+ });
150
+ if (listMissing.length > 0) {
151
+ missing[list.id] = {
152
+ name: list.name,
153
+ list: listMissing,
154
+ };
155
+ }
156
+ });
157
+ // Fringe vocabulary analysis
158
+ const fringeWords = this.analyzeFringe(targetWords, compareWords);
159
+ const commonFringeWords = this.analyzeCommonFringe(targetWords, compareWords);
160
+ return {
161
+ ...baseResult,
162
+ buttons: enrichedButtons,
163
+ // Target set metrics
164
+ target_effort_score: this.calculateEffortScore(targetResult),
165
+ // Comparison set metrics
166
+ comp_boards: compareResult.total_boards,
167
+ comp_buttons: compareResult.total_buttons,
168
+ comp_words: compareResult.total_words,
169
+ comp_grid: compareResult.grid,
170
+ comp_effort_score: this.calculateEffortScore(compareResult),
171
+ // Vocabulary comparison
172
+ missing_words: missingWords,
173
+ extra_words: extraWords,
174
+ overlapping_words: overlappingWords,
175
+ // Missing from lists
176
+ missing,
177
+ // High/low effort words
178
+ high_effort_words: highEffortWords.slice(0, 100),
179
+ low_effort_words: lowEffortWords.slice(0, 100),
180
+ // Core analysis
181
+ cores,
182
+ // CARE components
183
+ care_components: careComponents,
184
+ // Sentences
185
+ sentences,
186
+ // Fringe
187
+ fringe_words: fringeWords,
188
+ common_fringe_words: commonFringeWords,
189
+ };
190
+ }
191
+ /**
192
+ * Calculate CARE component scores
193
+ */
194
+ calculateCareComponents(targetResult, compareResult, _overlappingWords) {
195
+ // Create word maps
196
+ const targetWords = new Map();
197
+ targetResult.buttons.forEach((btn) => {
198
+ const existing = targetWords.get(btn.label);
199
+ if (!existing || btn.effort < existing.effort) {
200
+ targetWords.set(btn.label, btn);
201
+ }
202
+ });
203
+ const compareWords = new Map();
204
+ compareResult.buttons.forEach((btn) => {
205
+ const existing = compareWords.get(btn.label);
206
+ if (!existing || btn.effort < existing.effort) {
207
+ compareWords.set(btn.label, btn);
208
+ }
209
+ });
210
+ // Load reference data
211
+ const coreLists = this.referenceLoader.loadCoreLists();
212
+ const fringe = this.referenceLoader.loadFringe();
213
+ const sentences = this.referenceLoader.loadSentences();
214
+ // Calculate core coverage
215
+ let coreCount = 0;
216
+ let compCoreCount = 0;
217
+ const allCoreWords = new Set();
218
+ coreLists.forEach((list) => {
219
+ list.words.forEach((word) => allCoreWords.add(word.toLowerCase()));
220
+ });
221
+ allCoreWords.forEach((word) => {
222
+ if (targetWords.has(word))
223
+ coreCount++;
224
+ if (compareWords.has(word))
225
+ compCoreCount++;
226
+ });
227
+ // Calculate sentence construction effort
228
+ let sentenceEffort = 0;
229
+ let compSentenceEffort = 0;
230
+ let sentenceWordCount = 0;
231
+ sentences.forEach((words) => {
232
+ words.forEach((word) => {
233
+ const targetBtn = targetWords.get(word);
234
+ const compareBtn = compareWords.get(word);
235
+ if (targetBtn) {
236
+ sentenceEffort += targetBtn.effort;
237
+ }
238
+ else {
239
+ sentenceEffort += 10 + word.length * 2.5; // Spelling effort
240
+ }
241
+ if (compareBtn) {
242
+ compSentenceEffort += compareBtn.effort;
243
+ }
244
+ else {
245
+ compSentenceEffort += 10 + word.length * 2.5;
246
+ }
247
+ sentenceWordCount++;
248
+ });
249
+ });
250
+ const avgSentenceEffort = sentenceWordCount > 0 ? sentenceEffort / sentenceWordCount : 0;
251
+ const compAvgSentenceEffort = sentenceWordCount > 0 ? compSentenceEffort / sentenceWordCount : 0;
252
+ // Calculate fringe coverage
253
+ let fringeCount = 0;
254
+ let compFringeCount = 0;
255
+ let commonFringeCount = 0;
256
+ fringe.forEach((word) => {
257
+ const inTarget = targetWords.has(word);
258
+ const inCompare = compareWords.has(word);
259
+ if (inTarget)
260
+ fringeCount++;
261
+ if (inCompare)
262
+ compFringeCount++;
263
+ if (inTarget && inCompare)
264
+ commonFringeCount++;
265
+ });
266
+ return {
267
+ core: coreCount,
268
+ comp_core: compCoreCount,
269
+ sentences: avgSentenceEffort,
270
+ comp_sentences: compAvgSentenceEffort,
271
+ fringe: fringeCount,
272
+ comp_fringe: compFringeCount,
273
+ common_fringe: commonFringeCount,
274
+ comp_common_fringe: commonFringeCount,
275
+ };
276
+ }
277
+ /**
278
+ * Analyze fringe vocabulary
279
+ */
280
+ analyzeFringe(targetWords, compareWords) {
281
+ const fringe = this.referenceLoader.loadFringe();
282
+ const result = [];
283
+ fringe.forEach((word) => {
284
+ const targetBtn = targetWords.get(word);
285
+ const compareBtn = compareWords.get(word);
286
+ if (targetBtn) {
287
+ result.push({
288
+ word,
289
+ effort: targetBtn.effort,
290
+ comp_effort: compareBtn?.effort || 0,
291
+ });
292
+ }
293
+ });
294
+ result.sort((a, b) => a.effort - b.effort);
295
+ return result;
296
+ }
297
+ /**
298
+ * Analyze common fringe vocabulary
299
+ */
300
+ analyzeCommonFringe(targetWords, compareWords) {
301
+ const fringe = this.referenceLoader.loadFringe();
302
+ const result = [];
303
+ fringe.forEach((word) => {
304
+ const targetBtn = targetWords.get(word);
305
+ const compareBtn = compareWords.get(word);
306
+ if (targetBtn && compareBtn) {
307
+ result.push({
308
+ word,
309
+ effort: targetBtn.effort,
310
+ comp_effort: compareBtn.effort,
311
+ });
312
+ }
313
+ });
314
+ result.sort((a, b) => a.effort - b.effort);
315
+ return result;
316
+ }
317
+ /**
318
+ * Calculate overall effort score for a metrics result
319
+ */
320
+ calculateEffortScore(result) {
321
+ if (result.buttons.length === 0)
322
+ return 0;
323
+ let totalEffort = 0;
324
+ result.buttons.forEach((btn) => {
325
+ totalEffort += btn.effort;
326
+ });
327
+ return totalEffort / result.buttons.length;
328
+ }
329
+ }
330
+ exports.ComparisonAnalyzer = ComparisonAnalyzer;
@@ -0,0 +1,36 @@
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
+ * Analyze starting from a specific board
26
+ */
27
+ private analyzeFrom;
28
+ /**
29
+ * Calculate what percentage of links to this board match semantic_id/clone_id
30
+ */
31
+ private calculateBoardLinkPercentages;
32
+ /**
33
+ * Calculate grid dimensions from the tree
34
+ */
35
+ private calculateGridDimensions;
36
+ }