@willwade/aac-processors 0.0.21 ā 0.0.23
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 +19 -27
- package/dist/core/treeStructure.d.ts +2 -2
- package/dist/core/treeStructure.js +4 -1
- package/dist/processors/applePanelsProcessor.js +166 -123
- package/dist/processors/astericsGridProcessor.js +121 -105
- package/dist/processors/dotProcessor.js +83 -65
- package/dist/processors/gridsetProcessor.js +2 -0
- package/dist/processors/obfProcessor.js +11 -4
- package/dist/processors/opmlProcessor.js +82 -44
- package/dist/processors/snapProcessor.js +19 -9
- package/dist/processors/touchchatProcessor.js +72 -21
- package/dist/utilities/analytics/metrics/core.d.ts +1 -1
- package/dist/utilities/analytics/metrics/core.js +191 -212
- package/dist/validation/applePanelsValidator.d.ts +10 -0
- package/dist/validation/applePanelsValidator.js +124 -0
- package/dist/validation/astericsValidator.d.ts +16 -0
- package/dist/validation/astericsValidator.js +115 -0
- package/dist/validation/dotValidator.d.ts +10 -0
- package/dist/validation/dotValidator.js +113 -0
- package/dist/validation/excelValidator.d.ts +10 -0
- package/dist/validation/excelValidator.js +89 -0
- package/dist/validation/index.d.ts +14 -1
- package/dist/validation/index.js +104 -1
- package/dist/validation/obfsetValidator.d.ts +10 -0
- package/dist/validation/obfsetValidator.js +103 -0
- package/dist/validation/opmlValidator.d.ts +10 -0
- package/dist/validation/opmlValidator.js +107 -0
- package/dist/validation/validationTypes.d.ts +22 -0
- package/dist/validation/validationTypes.js +38 -1
- package/dist/validation.d.ts +8 -2
- package/dist/validation.js +16 -1
- package/package.json +1 -1
|
@@ -221,44 +221,26 @@ class MetricsCalculator {
|
|
|
221
221
|
* Count scan items for visual scanning effort
|
|
222
222
|
* When block scanning is enabled, count unique scan blocks instead of individual buttons
|
|
223
223
|
*/
|
|
224
|
-
|
|
225
|
-
if (!blockScanEnabled) {
|
|
226
|
-
// Linear scanning: count all buttons before current position
|
|
227
|
-
let count = 0;
|
|
228
|
-
for (let r = 0; r <= currentRowIndex; r++) {
|
|
229
|
-
const row = board.grid[r];
|
|
230
|
-
if (!row)
|
|
231
|
-
continue;
|
|
232
|
-
for (let c = 0; c < row.length; c++) {
|
|
233
|
-
if (r === currentRowIndex && c === currentColIndex)
|
|
234
|
-
return count;
|
|
235
|
-
const btn = row[c];
|
|
236
|
-
if (btn && (btn.label || btn.id).length > 0) {
|
|
237
|
-
count++;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return count;
|
|
242
|
-
}
|
|
224
|
+
countScanBlocks(board, currentRowIndex, currentColIndex, priorScanBlocks) {
|
|
243
225
|
// Block scanning: count unique scan blocks before current position
|
|
244
|
-
|
|
226
|
+
// Reuse the priorScanBlocks set from the parent scope
|
|
245
227
|
for (let r = 0; r <= currentRowIndex; r++) {
|
|
246
228
|
const row = board.grid[r];
|
|
247
229
|
if (!row)
|
|
248
230
|
continue;
|
|
249
231
|
for (let c = 0; c < row.length; c++) {
|
|
250
232
|
if (r === currentRowIndex && c === currentColIndex)
|
|
251
|
-
return
|
|
233
|
+
return priorScanBlocks.size;
|
|
252
234
|
const btn = row[c];
|
|
253
235
|
if (btn && (btn.label || btn.id).length > 0) {
|
|
254
236
|
const block = btn.scanBlock ||
|
|
255
237
|
(btn.scanBlocks && btn.scanBlocks.length > 0 ? btn.scanBlocks[0] : null);
|
|
256
238
|
if (block !== null)
|
|
257
|
-
|
|
239
|
+
priorScanBlocks.add(block);
|
|
258
240
|
}
|
|
259
241
|
}
|
|
260
242
|
}
|
|
261
|
-
return
|
|
243
|
+
return priorScanBlocks.size;
|
|
262
244
|
}
|
|
263
245
|
/**
|
|
264
246
|
* Analyze starting from a specific board
|
|
@@ -317,216 +299,213 @@ class MetricsCalculator {
|
|
|
317
299
|
const scanningConfig = options.scanningConfig || board.scanningConfig;
|
|
318
300
|
const blockScanEnabled = scanningConfig?.blockScanEnabled || false;
|
|
319
301
|
// Process each button
|
|
320
|
-
const priorScanBlocks = new Set();
|
|
321
302
|
const btnHeight = 1.0 / rows;
|
|
322
303
|
const btnWidth = 1.0 / cols;
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (btn.semantic_id && boardPcts[btn.semantic_id]) {
|
|
352
|
-
const discount = effort_1.EFFORT_CONSTANTS.SAME_LOCATION_AS_PRIOR_DISCOUNT / boardPcts[btn.semantic_id];
|
|
353
|
-
const old = buttonEffort;
|
|
354
|
-
buttonEffort = Math.min(buttonEffort, buttonEffort * discount);
|
|
355
|
-
if (debugSpecificButton)
|
|
356
|
-
console.log(` Semantic board discount: ${old.toFixed(6)} -> ${buttonEffort.toFixed(6)} (pct=${boardPcts[btn.semantic_id].toFixed(4)})`);
|
|
357
|
-
}
|
|
358
|
-
else if (btn.semantic_id && boardPcts[`upstream-${btn.semantic_id}`]) {
|
|
359
|
-
const discount = effort_1.EFFORT_CONSTANTS.RECOGNIZABLE_SEMANTIC_FROM_PRIOR_DISCOUNT /
|
|
360
|
-
boardPcts[`upstream-${btn.semantic_id}`];
|
|
361
|
-
const old = buttonEffort;
|
|
362
|
-
buttonEffort = Math.min(buttonEffort, buttonEffort * discount);
|
|
363
|
-
if (debugSpecificButton)
|
|
364
|
-
console.log(` Semantic upstream discount: ${old.toFixed(6)} -> ${buttonEffort.toFixed(6)} (pct=${boardPcts[`upstream-${btn.semantic_id}`].toFixed(4)})`);
|
|
365
|
-
}
|
|
366
|
-
// Apply clone_id discounts
|
|
367
|
-
if (btn.clone_id && boardPcts[btn.clone_id]) {
|
|
368
|
-
const discount = effort_1.EFFORT_CONSTANTS.SAME_LOCATION_AS_PRIOR_DISCOUNT / boardPcts[btn.clone_id];
|
|
369
|
-
buttonEffort = Math.min(buttonEffort, buttonEffort * discount);
|
|
370
|
-
}
|
|
371
|
-
else if (btn.clone_id && boardPcts[`upstream-${btn.clone_id}`]) {
|
|
372
|
-
const discount = effort_1.EFFORT_CONSTANTS.RECOGNIZABLE_CLONE_FROM_PRIOR_DISCOUNT /
|
|
373
|
-
boardPcts[`upstream-${btn.clone_id}`];
|
|
374
|
-
buttonEffort = Math.min(buttonEffort, buttonEffort * discount);
|
|
375
|
-
}
|
|
376
|
-
// Calculate button effort based on access method (Touch vs Scanning)
|
|
377
|
-
const isScanning = !!scanningConfig || !!board.scanType;
|
|
378
|
-
if (isScanning) {
|
|
379
|
-
const { steps, selections, loopSteps } = this.calculateScanSteps(board, btn, rowIndex, colIndex, scanningConfig);
|
|
380
|
-
// Determine effective costs based on selection method
|
|
381
|
-
let currentStepCost = options.scanStepCost ?? effort_1.EFFORT_CONSTANTS.SCAN_STEP_COST;
|
|
382
|
-
const currentSelectionCost = options.scanSelectionCost ?? effort_1.EFFORT_CONSTANTS.SCAN_SELECTION_COST;
|
|
383
|
-
// Step Scan 2 Switch: Every step is a physical selection with Switch 1
|
|
384
|
-
if (scanningConfig?.selectionMethod === aac_1.ScanningSelectionMethod.StepScan2Switch) {
|
|
385
|
-
// The cost of moving is now a selection cost
|
|
386
|
-
currentStepCost = currentSelectionCost;
|
|
387
|
-
}
|
|
388
|
-
else if (scanningConfig?.selectionMethod === aac_1.ScanningSelectionMethod.StepScan1Switch) {
|
|
389
|
-
// Single switch step scan: every step is a physical selection
|
|
390
|
-
currentStepCost = currentSelectionCost;
|
|
391
|
-
}
|
|
392
|
-
let sEffort = (0, effort_1.scanningEffort)(steps, selections, currentStepCost, currentSelectionCost);
|
|
393
|
-
// Factor in error correction if enabled
|
|
394
|
-
if (scanningConfig?.errorCorrectionEnabled) {
|
|
395
|
-
const errorRate = scanningConfig.errorRate ?? effort_1.EFFORT_CONSTANTS.DEFAULT_SCAN_ERROR_RATE;
|
|
396
|
-
// A "miss" results in needing to wait for a loop (or part of one)
|
|
397
|
-
// We model this as errorRate * (loopSteps * stepCost)
|
|
398
|
-
const retryPenalty = loopSteps * currentStepCost;
|
|
399
|
-
sEffort += errorRate * retryPenalty;
|
|
304
|
+
// Track scan blocks for block scanning
|
|
305
|
+
const priorScanBlocks = new Set();
|
|
306
|
+
// Iterate over grid positions directly (not just buttons)
|
|
307
|
+
// This matches Ruby's nested loop: rows.times do |row_idx|; columns.times do |col_idx|
|
|
308
|
+
for (let rowIndex = 0; rowIndex < rows; rowIndex++) {
|
|
309
|
+
for (let colIndex = 0; colIndex < cols; colIndex++) {
|
|
310
|
+
const btn = board.grid[rowIndex]?.[colIndex];
|
|
311
|
+
if (!btn)
|
|
312
|
+
continue; // Skip empty cells
|
|
313
|
+
const x = btnWidth / 2 + btnWidth * colIndex;
|
|
314
|
+
const y = btnHeight / 2 + btnHeight * rowIndex;
|
|
315
|
+
// Calculate prior grid positions (not just buttons)
|
|
316
|
+
// This matches Ruby's prior_buttons which increments for each grid position
|
|
317
|
+
const priorGridPositions = rowIndex * cols + colIndex;
|
|
318
|
+
// For block scanning, count unique scan blocks instead
|
|
319
|
+
const priorItems = blockScanEnabled
|
|
320
|
+
? this.countScanBlocks(board, rowIndex, colIndex, priorScanBlocks)
|
|
321
|
+
: priorGridPositions;
|
|
322
|
+
// Calculate button-level effort
|
|
323
|
+
let buttonEffort = boardEffort;
|
|
324
|
+
// Debug for specific button (disabled for production)
|
|
325
|
+
const debugSpecificButton = btn.label === '$938c2cc0dc';
|
|
326
|
+
if (debugSpecificButton) {
|
|
327
|
+
console.log(`\nš DEBUG Button ${btn.label} at [${rowIndex},${colIndex}] on ${board.id}:`);
|
|
328
|
+
console.log(` Entry point: (${entryX.toFixed(4)}, ${entryY.toFixed(4)})`);
|
|
329
|
+
console.log(` Current level: ${level}`);
|
|
330
|
+
console.log(` Prior positions: ${priorItems}`);
|
|
331
|
+
console.log(` Starting effort: ${buttonEffort.toFixed(6)}`);
|
|
400
332
|
}
|
|
401
|
-
// Apply discounts
|
|
333
|
+
// Apply semantic_id discounts
|
|
402
334
|
if (btn.semantic_id && boardPcts[btn.semantic_id]) {
|
|
403
335
|
const discount = effort_1.EFFORT_CONSTANTS.SAME_LOCATION_AS_PRIOR_DISCOUNT / boardPcts[btn.semantic_id];
|
|
404
|
-
|
|
336
|
+
const old = buttonEffort;
|
|
337
|
+
buttonEffort = Math.min(buttonEffort, buttonEffort * discount);
|
|
338
|
+
if (debugSpecificButton)
|
|
339
|
+
console.log(` Semantic board discount: ${old.toFixed(6)} -> ${buttonEffort.toFixed(6)} (pct=${boardPcts[btn.semantic_id].toFixed(4)})`);
|
|
405
340
|
}
|
|
406
|
-
else if (btn.
|
|
341
|
+
else if (btn.semantic_id && boardPcts[`upstream-${btn.semantic_id}`]) {
|
|
342
|
+
const discount = effort_1.EFFORT_CONSTANTS.RECOGNIZABLE_SEMANTIC_FROM_PRIOR_DISCOUNT /
|
|
343
|
+
boardPcts[`upstream-${btn.semantic_id}`];
|
|
344
|
+
const old = buttonEffort;
|
|
345
|
+
buttonEffort = Math.min(buttonEffort, buttonEffort * discount);
|
|
346
|
+
if (debugSpecificButton)
|
|
347
|
+
console.log(` Semantic upstream discount: ${old.toFixed(6)} -> ${buttonEffort.toFixed(6)} (pct=${boardPcts[`upstream-${btn.semantic_id}`].toFixed(4)})`);
|
|
348
|
+
}
|
|
349
|
+
// Apply clone_id discounts
|
|
350
|
+
if (btn.clone_id && boardPcts[btn.clone_id]) {
|
|
407
351
|
const discount = effort_1.EFFORT_CONSTANTS.SAME_LOCATION_AS_PRIOR_DISCOUNT / boardPcts[btn.clone_id];
|
|
408
|
-
|
|
352
|
+
buttonEffort = Math.min(buttonEffort, buttonEffort * discount);
|
|
409
353
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
354
|
+
else if (btn.clone_id && boardPcts[`upstream-${btn.clone_id}`]) {
|
|
355
|
+
const discount = effort_1.EFFORT_CONSTANTS.RECOGNIZABLE_CLONE_FROM_PRIOR_DISCOUNT /
|
|
356
|
+
boardPcts[`upstream-${btn.clone_id}`];
|
|
357
|
+
buttonEffort = Math.min(buttonEffort, buttonEffort * discount);
|
|
358
|
+
}
|
|
359
|
+
// Calculate button effort based on access method (Touch vs Scanning)
|
|
360
|
+
const isScanning = !!scanningConfig || !!board.scanType;
|
|
361
|
+
if (isScanning) {
|
|
362
|
+
const { steps, selections, loopSteps } = this.calculateScanSteps(board, btn, rowIndex, colIndex, scanningConfig);
|
|
363
|
+
// Determine effective costs based on selection method
|
|
364
|
+
let currentStepCost = options.scanStepCost ?? effort_1.EFFORT_CONSTANTS.SCAN_STEP_COST;
|
|
365
|
+
const currentSelectionCost = options.scanSelectionCost ?? effort_1.EFFORT_CONSTANTS.SCAN_SELECTION_COST;
|
|
366
|
+
// Step Scan 2 Switch: Every step is a physical selection with Switch 1
|
|
367
|
+
if (scanningConfig?.selectionMethod === aac_1.ScanningSelectionMethod.StepScan2Switch) {
|
|
368
|
+
// The cost of moving is now a selection cost
|
|
369
|
+
currentStepCost = currentSelectionCost;
|
|
425
370
|
}
|
|
426
|
-
else if (
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
distance = Math.min(distance, distance * discount);
|
|
371
|
+
else if (scanningConfig?.selectionMethod === aac_1.ScanningSelectionMethod.StepScan1Switch) {
|
|
372
|
+
// Single switch step scan: every step is a physical selection
|
|
373
|
+
currentStepCost = currentSelectionCost;
|
|
430
374
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (
|
|
434
|
-
const
|
|
435
|
-
|
|
375
|
+
let sEffort = (0, effort_1.scanningEffort)(steps, selections, currentStepCost, currentSelectionCost);
|
|
376
|
+
// Factor in error correction if enabled
|
|
377
|
+
if (scanningConfig?.errorCorrectionEnabled) {
|
|
378
|
+
const errorRate = scanningConfig.errorRate ?? effort_1.EFFORT_CONSTANTS.DEFAULT_SCAN_ERROR_RATE;
|
|
379
|
+
// A "miss" results in needing to wait for a loop (or part of one)
|
|
380
|
+
// We model this as errorRate * (loopSteps * stepCost)
|
|
381
|
+
const retryPenalty = loopSteps * currentStepCost;
|
|
382
|
+
sEffort += errorRate * retryPenalty;
|
|
436
383
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
384
|
+
// Apply discounts to scanning effort (similar to touch)
|
|
385
|
+
if (btn.semantic_id && boardPcts[btn.semantic_id]) {
|
|
386
|
+
const discount = effort_1.EFFORT_CONSTANTS.SAME_LOCATION_AS_PRIOR_DISCOUNT / boardPcts[btn.semantic_id];
|
|
387
|
+
sEffort = Math.min(sEffort, sEffort * discount);
|
|
441
388
|
}
|
|
442
|
-
else if (
|
|
443
|
-
const discount = effort_1.EFFORT_CONSTANTS.
|
|
444
|
-
|
|
389
|
+
else if (btn.clone_id && boardPcts[btn.clone_id]) {
|
|
390
|
+
const discount = effort_1.EFFORT_CONSTANTS.SAME_LOCATION_AS_PRIOR_DISCOUNT / boardPcts[btn.clone_id];
|
|
391
|
+
sEffort = Math.min(sEffort, sEffort * discount);
|
|
445
392
|
}
|
|
446
|
-
|
|
447
|
-
buttonEffort += distance;
|
|
448
|
-
// Add visual scan or local scan effort
|
|
449
|
-
if (distance > effort_1.EFFORT_CONSTANTS.DISTANCE_THRESHOLD_TO_SKIP_VISUAL_SCAN ||
|
|
450
|
-
(entryX === 1.0 && entryY === 1.0)) {
|
|
451
|
-
buttonEffort += (0, effort_1.visualScanEffort)(priorItems);
|
|
393
|
+
buttonEffort += sEffort;
|
|
452
394
|
}
|
|
453
395
|
else {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
396
|
+
// Add distance effort (Touch only)
|
|
397
|
+
let distance = (0, effort_1.distanceEffort)(x, y, entryX, entryY);
|
|
398
|
+
// Apply distance discounts
|
|
399
|
+
if (btn.semantic_id) {
|
|
400
|
+
if (boardPcts[btn.semantic_id]) {
|
|
401
|
+
const discount = effort_1.EFFORT_CONSTANTS.SAME_LOCATION_AS_PRIOR_DISCOUNT / boardPcts[btn.semantic_id];
|
|
402
|
+
distance = Math.min(distance, distance * discount);
|
|
403
|
+
}
|
|
404
|
+
else if (boardPcts[`upstream-${btn.semantic_id}`]) {
|
|
405
|
+
const discount = effort_1.EFFORT_CONSTANTS.RECOGNIZABLE_SEMANTIC_FROM_PRIOR_DISCOUNT /
|
|
406
|
+
boardPcts[`upstream-${btn.semantic_id}`];
|
|
407
|
+
distance = Math.min(distance, distance * discount);
|
|
408
|
+
}
|
|
409
|
+
else if (level > 0 && setPcts[btn.semantic_id]) {
|
|
410
|
+
const discount = effort_1.EFFORT_CONSTANTS.RECOGNIZABLE_SEMANTIC_FROM_OTHER_DISCOUNT /
|
|
411
|
+
setPcts[btn.semantic_id];
|
|
412
|
+
distance = Math.min(distance, distance * discount);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (btn.clone_id) {
|
|
416
|
+
if (boardPcts[btn.clone_id]) {
|
|
417
|
+
const discount = effort_1.EFFORT_CONSTANTS.SAME_LOCATION_AS_PRIOR_DISCOUNT / boardPcts[btn.clone_id];
|
|
418
|
+
distance = Math.min(distance, distance * discount);
|
|
419
|
+
}
|
|
420
|
+
else if (boardPcts[`upstream-${btn.clone_id}`]) {
|
|
421
|
+
const discount = effort_1.EFFORT_CONSTANTS.RECOGNIZABLE_CLONE_FROM_PRIOR_DISCOUNT /
|
|
422
|
+
boardPcts[`upstream-${btn.clone_id}`];
|
|
423
|
+
distance = Math.min(distance, distance * discount);
|
|
424
|
+
}
|
|
425
|
+
else if (level > 0 && setPcts[btn.clone_id]) {
|
|
426
|
+
const discount = effort_1.EFFORT_CONSTANTS.RECOGNIZABLE_CLONE_FROM_OTHER_DISCOUNT / setPcts[btn.clone_id];
|
|
427
|
+
distance = Math.min(distance, distance * discount);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
buttonEffort += distance;
|
|
431
|
+
// Add visual scan or local scan effort
|
|
432
|
+
if (distance > effort_1.EFFORT_CONSTANTS.DISTANCE_THRESHOLD_TO_SKIP_VISUAL_SCAN ||
|
|
433
|
+
(entryX === 1.0 && entryY === 1.0)) {
|
|
434
|
+
buttonEffort += (0, effort_1.visualScanEffort)(priorItems);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
buttonEffort += (0, effort_1.localScanEffort)(distance);
|
|
490
438
|
}
|
|
491
439
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const addToSentence = btn.semanticAction?.platformData?.grid3?.parameters?.add_to_sentence ||
|
|
501
|
-
btn.semanticAction?.fallback?.add_to_sentence;
|
|
502
|
-
if (isSpeak || addToSentence) {
|
|
503
|
-
let finalEffort = buttonEffort;
|
|
504
|
-
// Apply Board Change Processing Effort Discount (matching Ruby lines 347-350)
|
|
505
|
-
const changeEffort = effort_1.EFFORT_CONSTANTS.BOARD_CHANGE_PROCESSING_EFFORT;
|
|
506
|
-
if (btn.clone_id && boardPcts[btn.clone_id]) {
|
|
507
|
-
const discount = Math.min(changeEffort, (changeEffort * 0.3) / boardPcts[btn.clone_id]);
|
|
508
|
-
finalEffort -= discount;
|
|
440
|
+
// Add cumulative prior effort
|
|
441
|
+
buttonEffort += priorEffort;
|
|
442
|
+
// Track scan blocks for block scanning, otherwise track individual buttons
|
|
443
|
+
if (blockScanEnabled) {
|
|
444
|
+
const scanBlockId = btn.scanBlock ?? btn.scanBlocks?.[0];
|
|
445
|
+
if (scanBlockId !== undefined && scanBlockId !== null) {
|
|
446
|
+
priorScanBlocks.add(scanBlockId);
|
|
447
|
+
}
|
|
509
448
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
449
|
+
// Handle navigation buttons
|
|
450
|
+
if (btn.targetPageId) {
|
|
451
|
+
const nextBoard = tree.getPage(btn.targetPageId);
|
|
452
|
+
if (nextBoard) {
|
|
453
|
+
// Only add to toVisit if this board hasn't been visited yet at any level
|
|
454
|
+
// The visitedBoardIds map stores the *lowest* level a board was visited.
|
|
455
|
+
// If it's already in the map, it means we've processed it or scheduled it at a lower level.
|
|
456
|
+
if (visitedBoardIds.get(nextBoard.id) === undefined) {
|
|
457
|
+
const changeEffort = effort_1.EFFORT_CONSTANTS.BOARD_CHANGE_PROCESSING_EFFORT;
|
|
458
|
+
const tempHomeId = btn.semanticAction?.platformData?.grid3?.parameters?.temporary_home === 'prior'
|
|
459
|
+
? board.id
|
|
460
|
+
: btn.semanticAction?.platformData?.grid3?.parameters?.temporary_home === true
|
|
461
|
+
? btn.targetPageId
|
|
462
|
+
: temporaryHomeId;
|
|
463
|
+
toVisit.push({
|
|
464
|
+
board: nextBoard,
|
|
465
|
+
level: level + 1,
|
|
466
|
+
priorEffort: buttonEffort + changeEffort,
|
|
467
|
+
temporaryHomeId: tempHomeId,
|
|
468
|
+
entryX: x,
|
|
469
|
+
entryY: y,
|
|
470
|
+
entryCloneId: btn.clone_id,
|
|
471
|
+
entrySemanticId: btn.semantic_id,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
513
475
|
}
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
clone_id
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
476
|
+
// Track word if it speaks or adds to sentence
|
|
477
|
+
const isSpeak = btn.semanticAction?.category === treeStructure_1.AACSemanticCategory.COMMUNICATION && !btn.targetPageId; // Must not be a navigation button
|
|
478
|
+
const addToSentence = btn.semanticAction?.platformData?.grid3?.parameters?.add_to_sentence ||
|
|
479
|
+
btn.semanticAction?.fallback?.add_to_sentence;
|
|
480
|
+
if (isSpeak || addToSentence) {
|
|
481
|
+
let finalEffort = buttonEffort;
|
|
482
|
+
// Apply Board Change Processing Effort Discount (matching Ruby lines 347-350)
|
|
483
|
+
const changeEffort = effort_1.EFFORT_CONSTANTS.BOARD_CHANGE_PROCESSING_EFFORT;
|
|
484
|
+
if (btn.clone_id && boardPcts[btn.clone_id]) {
|
|
485
|
+
const discount = Math.min(changeEffort, (changeEffort * 0.3) / boardPcts[btn.clone_id]);
|
|
486
|
+
finalEffort -= discount;
|
|
487
|
+
}
|
|
488
|
+
else if (btn.semantic_id && boardPcts[btn.semantic_id]) {
|
|
489
|
+
const discount = Math.min(changeEffort, (changeEffort * 0.5) / boardPcts[btn.semantic_id]);
|
|
490
|
+
finalEffort -= discount;
|
|
491
|
+
}
|
|
492
|
+
const existing = knownButtons.get(btn.label);
|
|
493
|
+
const knownBtn = {
|
|
494
|
+
id: btn.id,
|
|
495
|
+
label: btn.label,
|
|
496
|
+
level,
|
|
497
|
+
effort: finalEffort,
|
|
498
|
+
count: (existing?.count || 0) + 1,
|
|
499
|
+
semantic_id: btn.semantic_id,
|
|
500
|
+
clone_id: btn.clone_id,
|
|
501
|
+
temporary_home_id: temporaryHomeId || undefined,
|
|
502
|
+
};
|
|
503
|
+
if (!existing || finalEffort < existing.effort) {
|
|
504
|
+
knownButtons.set(btn.label, knownBtn);
|
|
505
|
+
}
|
|
527
506
|
}
|
|
528
507
|
}
|
|
529
|
-
}
|
|
508
|
+
}
|
|
530
509
|
}
|
|
531
510
|
// Convert to array and group by level
|
|
532
511
|
const buttons = Array.from(knownButtons.values());
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseValidator } from './baseValidator';
|
|
2
|
+
import { ValidationResult } from './validationTypes';
|
|
3
|
+
/**
|
|
4
|
+
* Validator for Apple Panels (.plist or .ascconfig directory)
|
|
5
|
+
*/
|
|
6
|
+
export declare class ApplePanelsValidator extends BaseValidator {
|
|
7
|
+
static validateFile(filePath: string): Promise<ValidationResult>;
|
|
8
|
+
static identifyFormat(content: any, filename: string): Promise<boolean>;
|
|
9
|
+
validate(content: Buffer | Uint8Array, filename: string, filesize: number): Promise<ValidationResult>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.ApplePanelsValidator = void 0;
|
|
30
|
+
/* eslint-disable @typescript-eslint/require-await */
|
|
31
|
+
const fs = __importStar(require("fs"));
|
|
32
|
+
const path = __importStar(require("path"));
|
|
33
|
+
const plist_1 = __importDefault(require("plist"));
|
|
34
|
+
const baseValidator_1 = require("./baseValidator");
|
|
35
|
+
/**
|
|
36
|
+
* Validator for Apple Panels (.plist or .ascconfig directory)
|
|
37
|
+
*/
|
|
38
|
+
class ApplePanelsValidator extends baseValidator_1.BaseValidator {
|
|
39
|
+
static async validateFile(filePath) {
|
|
40
|
+
const validator = new ApplePanelsValidator();
|
|
41
|
+
let content;
|
|
42
|
+
const filename = path.basename(filePath);
|
|
43
|
+
let size = 0;
|
|
44
|
+
const stats = fs.existsSync(filePath) ? fs.statSync(filePath) : null;
|
|
45
|
+
if (stats?.isDirectory() && filename.toLowerCase().endsWith('.ascconfig')) {
|
|
46
|
+
const panelPath = path.join(filePath, 'Contents', 'Resources', 'PanelDefinitions.plist');
|
|
47
|
+
if (!fs.existsSync(panelPath)) {
|
|
48
|
+
return validator.validate(Buffer.alloc(0), filename, 0);
|
|
49
|
+
}
|
|
50
|
+
content = fs.readFileSync(panelPath);
|
|
51
|
+
size = fs.statSync(panelPath).size;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
content = fs.readFileSync(filePath);
|
|
55
|
+
size = stats?.size || content.byteLength;
|
|
56
|
+
}
|
|
57
|
+
return validator.validate(content, filename, size);
|
|
58
|
+
}
|
|
59
|
+
static async identifyFormat(content, filename) {
|
|
60
|
+
const name = filename.toLowerCase();
|
|
61
|
+
if (name.endsWith('.plist') || name.endsWith('.ascconfig')) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
|
|
66
|
+
const parsed = plist_1.default.parse(str);
|
|
67
|
+
return Boolean(parsed.panels || parsed.Panels);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async validate(content, filename, filesize) {
|
|
74
|
+
this.reset();
|
|
75
|
+
await this.add_check('filename', 'file extension', async () => {
|
|
76
|
+
if (!filename.toLowerCase().match(/\.(plist|ascconfig)$/)) {
|
|
77
|
+
this.warn('filename should end with .plist or .ascconfig');
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
let parsed = null;
|
|
81
|
+
await this.add_check('plist_parse', 'valid plist/XML', async () => {
|
|
82
|
+
try {
|
|
83
|
+
const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
|
|
84
|
+
parsed = plist_1.default.parse(str);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
this.err(`Failed to parse plist: ${e.message}`, true);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
if (!parsed) {
|
|
91
|
+
return this.buildResult(filename, filesize, 'applepanels');
|
|
92
|
+
}
|
|
93
|
+
let panels = [];
|
|
94
|
+
await this.add_check('panels', 'panels present', async () => {
|
|
95
|
+
if (Array.isArray(parsed?.panels)) {
|
|
96
|
+
panels = parsed?.panels;
|
|
97
|
+
}
|
|
98
|
+
else if (parsed?.Panels && typeof parsed.Panels === 'object') {
|
|
99
|
+
panels = Object.values(parsed.Panels);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
this.err('missing panels/PanelDefinitions content', true);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
panels.slice(0, 5).forEach((panel, idx) => {
|
|
106
|
+
const prefix = `panel[${idx}]`;
|
|
107
|
+
this.add_check_sync(`${prefix}_id`, `${prefix} id`, () => {
|
|
108
|
+
if (!panel?.ID && !panel?.id) {
|
|
109
|
+
this.err('panel missing ID');
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
this.add_check_sync(`${prefix}_buttons`, `${prefix} buttons`, () => {
|
|
113
|
+
const buttons = Array.isArray(panel?.PanelObjects)
|
|
114
|
+
? panel.PanelObjects.filter((obj) => obj?.PanelObjectType === 'Button')
|
|
115
|
+
: [];
|
|
116
|
+
if (buttons.length === 0) {
|
|
117
|
+
this.warn('panel has no buttons');
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
return this.buildResult(filename, filesize, 'applepanels');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.ApplePanelsValidator = ApplePanelsValidator;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseValidator } from './baseValidator';
|
|
2
|
+
import { ValidationResult } from './validationTypes';
|
|
3
|
+
/**
|
|
4
|
+
* Validator for Asterics Grid (.grd) JSON files
|
|
5
|
+
*/
|
|
6
|
+
export declare class AstericsGridValidator extends BaseValidator {
|
|
7
|
+
/**
|
|
8
|
+
* Validate from disk
|
|
9
|
+
*/
|
|
10
|
+
static validateFile(filePath: string): Promise<ValidationResult>;
|
|
11
|
+
/**
|
|
12
|
+
* Identify whether the content appears to be an Asterics .grd file
|
|
13
|
+
*/
|
|
14
|
+
static identifyFormat(content: any, filename: string): Promise<boolean>;
|
|
15
|
+
validate(content: Buffer | Uint8Array, filename: string, filesize: number): Promise<ValidationResult>;
|
|
16
|
+
}
|