@vascend/talos 0.1.0 → 0.1.2

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 (66) hide show
  1. package/README.md +7 -3
  2. package/dist/backend/asset-events.js +9 -9
  3. package/dist/core/agent.js +3 -3
  4. package/dist/core/cli-args.js +50 -50
  5. package/dist/core/cli-installation.d.ts +4 -0
  6. package/dist/core/cli-installation.js +55 -0
  7. package/dist/core/compaction.js +26 -26
  8. package/dist/core/config.js +101 -101
  9. package/dist/core/execution-guidance.js +21 -21
  10. package/dist/core/package-update.d.ts +3 -1
  11. package/dist/core/package-update.js +19 -6
  12. package/dist/core/self-update.d.ts +1 -1
  13. package/dist/core/signed-release.d.ts +33 -0
  14. package/dist/core/signed-release.js +123 -0
  15. package/dist/core/update-check.d.ts +1 -1
  16. package/dist/core/update-check.js +15 -14
  17. package/dist/core/vibe-modes.js +13 -13
  18. package/dist/distribution/install.d.ts +1 -0
  19. package/dist/distribution/install.js +31 -0
  20. package/dist/install.mjs +271 -0
  21. package/dist/node-runtime.json +13 -0
  22. package/dist/providers/oauth-callback-server.js +32 -32
  23. package/dist/tools/capabilities.js +30 -30
  24. package/dist/tools/computer-capture.js +50 -50
  25. package/dist/tools/program.js +82 -82
  26. package/dist/tui/i18n-catalog.js +16 -0
  27. package/dist/tui/quick-help.js +31 -31
  28. package/dist/tui/repl.js +5 -5
  29. package/dist/tui/update-prompt.js +1 -0
  30. package/dist/vascend/code-events.js +9 -9
  31. package/dist/vascend/completeness.js +31 -31
  32. package/dist/vascend/context-fabric.js +16 -16
  33. package/dist/vascend/contract-memory.js +30 -30
  34. package/dist/vascend/council.js +82 -82
  35. package/dist/vascend/decision-promotion.js +23 -23
  36. package/dist/vascend/granular-kingdom.js +17 -17
  37. package/dist/vascend/granular-launcher.js +2 -2
  38. package/dist/vascend/kingdom-kpi.js +19 -19
  39. package/dist/vascend/kingdom-run.js +6 -6
  40. package/dist/vascend/kingdom.js +56 -56
  41. package/dist/vascend/mandate.js +21 -21
  42. package/dist/vascend/objective-revision.js +34 -34
  43. package/dist/vascend/patch-plan.js +22 -22
  44. package/dist/vascend/requirements.js +29 -29
  45. package/dist/vascend/reviewer.js +26 -26
  46. package/dist/vascend/room-memory.js +25 -25
  47. package/dist/vascend/runtime.js +44 -44
  48. package/dist/vascend/self-improve.js +39 -39
  49. package/dist/vascend/test-failure-memory.js +9 -9
  50. package/dist/vascend/vision.js +18 -18
  51. package/dist/vendor/computer-use/README.md +7 -7
  52. package/dist/vendor/computer-use/computer-desktop.js +211 -211
  53. package/dist/vendor/computer-use/computer-observe.js +315 -315
  54. package/dist/vendor/computer-use/computer-scripts.js +1230 -1230
  55. package/dist/vendor/computer-use/computer-tools.js +2136 -2136
  56. package/dist/vendor/computer-use/package.json +1 -1
  57. package/dist/vendor/computer-use/rac/atomic-input.js +91 -91
  58. package/dist/vendor/computer-use/rac/grammatica.js +101 -101
  59. package/dist/vendor/computer-use/rac/parser.js +185 -185
  60. package/dist/vendor/computer-use/rac/protocol.js +136 -136
  61. package/install.ps1 +374 -30
  62. package/install.sh +356 -43
  63. package/package.json +2 -2
  64. package/dist/core/kingdoms-mode-state.d.ts +0 -24
  65. package/dist/core/kingdoms-mode-state.js +0 -139
  66. package/dist/text-sanitize.js +0 -28
@@ -1 +1 @@
1
- {"type":"commonjs","private":true}
1
+ {"type":"commonjs","private":true}
@@ -1,91 +1,91 @@
1
- 'use strict';
2
-
3
- const MAX_OPERATIONS = 12;
4
- const MAX_TEXT = 4000;
5
-
6
- const KEY_ALIASES = Object.freeze({
7
- ctrl: 'Ctrl', control: 'Ctrl', shift: 'Shift', alt: 'Alt',
8
- space: 'Space', tab: 'Tab', esc: 'Esc', escape: 'Esc',
9
- enter: 'Enter', return: 'Enter', backspace: 'Backspace', delete: 'Delete', del: 'Delete',
10
- home: 'Home', end: 'End', left: 'ArrowLeft', arrowleft: 'ArrowLeft',
11
- up: 'ArrowUp', arrowup: 'ArrowUp', right: 'ArrowRight', arrowright: 'ArrowRight',
12
- down: 'ArrowDown', arrowdown: 'ArrowDown',
13
- });
14
-
15
- function normalizeKey(value) {
16
- const raw = String(value || '').trim();
17
- if (!raw) return null;
18
- const alias = KEY_ALIASES[raw.toLowerCase()];
19
- if (alias) return alias;
20
- if (/^[a-z0-9]$/i.test(raw)) return raw.toUpperCase();
21
- return null;
22
- }
23
-
24
- function normalizeShortcut(keys) {
25
- const source = Array.isArray(keys)
26
- ? keys
27
- : String(keys || '').split(/[+\s]+/).filter(Boolean);
28
- if (!source.length || source.length > 4) {
29
- return { ok: false, code: 'BAD_SHORTCUT', error: 'Una scorciatoia richiede da uno a quattro tasti.' };
30
- }
31
- const normalized = [];
32
- for (const value of source) {
33
- const key = normalizeKey(value);
34
- if (!key) return { ok: false, code: 'BAD_SHORTCUT', error: `Tasto non ammesso: ${String(value)}` };
35
- if (!normalized.includes(key)) normalized.push(key);
36
- }
37
- return { ok: true, keys: normalized };
38
- }
39
-
40
- function normalizeAtomicOperations(value) {
41
- if (!Array.isArray(value) || !value.length || value.length > MAX_OPERATIONS) {
42
- return { ok: false, code: 'BAD_OPERATIONS', error: `Servono da 1 a ${MAX_OPERATIONS} operazioni.` };
43
- }
44
- const operations = [];
45
- for (const raw of value) {
46
- const type = String(raw && raw.type || '').trim().toLowerCase();
47
- if (type === 'shortcut') {
48
- const shortcut = normalizeShortcut(raw.keys);
49
- if (!shortcut.ok) return shortcut;
50
- operations.push({ type, keys: shortcut.keys });
51
- continue;
52
- }
53
- if (type === 'text') {
54
- const text = String(raw && raw.text != null ? raw.text : '');
55
- if (!text || text.length > MAX_TEXT || /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/.test(text)) {
56
- return { ok: false, code: 'BAD_TEXT', error: `Il testo deve contenere da 1 a ${MAX_TEXT} caratteri stampabili.` };
57
- }
58
- operations.push({ type, text });
59
- continue;
60
- }
61
- if (type === 'mouse') {
62
- const dx = Number(raw && raw.dx || 0);
63
- const dy = Number(raw && raw.dy || 0);
64
- const scroll = Number(raw && raw.scroll || 0);
65
- if (![dx, dy, scroll].every(Number.isInteger)
66
- || Math.abs(dx) > 1000 || Math.abs(dy) > 1000 || Math.abs(scroll) > 5) {
67
- return { ok: false, code: 'BAD_MOUSE', error: 'Mouse fuori range: dx/dy ±1000, scroll ±5.' };
68
- }
69
- operations.push({ type, dx, dy, scroll });
70
- continue;
71
- }
72
- if (type === 'click') {
73
- const button = String(raw && raw.button || 'left').toLowerCase();
74
- if (!['left', 'right', 'middle'].includes(button)) {
75
- return { ok: false, code: 'BAD_BUTTON', error: 'Pulsante ammesso: left, right o middle.' };
76
- }
77
- operations.push({ type, button });
78
- continue;
79
- }
80
- return { ok: false, code: 'BAD_OPERATION', error: `Operazione non ammessa: ${type || '(vuota)'}` };
81
- }
82
- return { ok: true, operations };
83
- }
84
-
85
- module.exports = {
86
- MAX_OPERATIONS,
87
- MAX_TEXT,
88
- normalizeKey,
89
- normalizeShortcut,
90
- normalizeAtomicOperations,
91
- };
1
+ 'use strict';
2
+
3
+ const MAX_OPERATIONS = 12;
4
+ const MAX_TEXT = 4000;
5
+
6
+ const KEY_ALIASES = Object.freeze({
7
+ ctrl: 'Ctrl', control: 'Ctrl', shift: 'Shift', alt: 'Alt',
8
+ space: 'Space', tab: 'Tab', esc: 'Esc', escape: 'Esc',
9
+ enter: 'Enter', return: 'Enter', backspace: 'Backspace', delete: 'Delete', del: 'Delete',
10
+ home: 'Home', end: 'End', left: 'ArrowLeft', arrowleft: 'ArrowLeft',
11
+ up: 'ArrowUp', arrowup: 'ArrowUp', right: 'ArrowRight', arrowright: 'ArrowRight',
12
+ down: 'ArrowDown', arrowdown: 'ArrowDown',
13
+ });
14
+
15
+ function normalizeKey(value) {
16
+ const raw = String(value || '').trim();
17
+ if (!raw) return null;
18
+ const alias = KEY_ALIASES[raw.toLowerCase()];
19
+ if (alias) return alias;
20
+ if (/^[a-z0-9]$/i.test(raw)) return raw.toUpperCase();
21
+ return null;
22
+ }
23
+
24
+ function normalizeShortcut(keys) {
25
+ const source = Array.isArray(keys)
26
+ ? keys
27
+ : String(keys || '').split(/[+\s]+/).filter(Boolean);
28
+ if (!source.length || source.length > 4) {
29
+ return { ok: false, code: 'BAD_SHORTCUT', error: 'Una scorciatoia richiede da uno a quattro tasti.' };
30
+ }
31
+ const normalized = [];
32
+ for (const value of source) {
33
+ const key = normalizeKey(value);
34
+ if (!key) return { ok: false, code: 'BAD_SHORTCUT', error: `Tasto non ammesso: ${String(value)}` };
35
+ if (!normalized.includes(key)) normalized.push(key);
36
+ }
37
+ return { ok: true, keys: normalized };
38
+ }
39
+
40
+ function normalizeAtomicOperations(value) {
41
+ if (!Array.isArray(value) || !value.length || value.length > MAX_OPERATIONS) {
42
+ return { ok: false, code: 'BAD_OPERATIONS', error: `Servono da 1 a ${MAX_OPERATIONS} operazioni.` };
43
+ }
44
+ const operations = [];
45
+ for (const raw of value) {
46
+ const type = String(raw && raw.type || '').trim().toLowerCase();
47
+ if (type === 'shortcut') {
48
+ const shortcut = normalizeShortcut(raw.keys);
49
+ if (!shortcut.ok) return shortcut;
50
+ operations.push({ type, keys: shortcut.keys });
51
+ continue;
52
+ }
53
+ if (type === 'text') {
54
+ const text = String(raw && raw.text != null ? raw.text : '');
55
+ if (!text || text.length > MAX_TEXT || /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/.test(text)) {
56
+ return { ok: false, code: 'BAD_TEXT', error: `Il testo deve contenere da 1 a ${MAX_TEXT} caratteri stampabili.` };
57
+ }
58
+ operations.push({ type, text });
59
+ continue;
60
+ }
61
+ if (type === 'mouse') {
62
+ const dx = Number(raw && raw.dx || 0);
63
+ const dy = Number(raw && raw.dy || 0);
64
+ const scroll = Number(raw && raw.scroll || 0);
65
+ if (![dx, dy, scroll].every(Number.isInteger)
66
+ || Math.abs(dx) > 1000 || Math.abs(dy) > 1000 || Math.abs(scroll) > 5) {
67
+ return { ok: false, code: 'BAD_MOUSE', error: 'Mouse fuori range: dx/dy ±1000, scroll ±5.' };
68
+ }
69
+ operations.push({ type, dx, dy, scroll });
70
+ continue;
71
+ }
72
+ if (type === 'click') {
73
+ const button = String(raw && raw.button || 'left').toLowerCase();
74
+ if (!['left', 'right', 'middle'].includes(button)) {
75
+ return { ok: false, code: 'BAD_BUTTON', error: 'Pulsante ammesso: left, right o middle.' };
76
+ }
77
+ operations.push({ type, button });
78
+ continue;
79
+ }
80
+ return { ok: false, code: 'BAD_OPERATION', error: `Operazione non ammessa: ${type || '(vuota)'}` };
81
+ }
82
+ return { ok: true, operations };
83
+ }
84
+
85
+ module.exports = {
86
+ MAX_OPERATIONS,
87
+ MAX_TEXT,
88
+ normalizeKey,
89
+ normalizeShortcut,
90
+ normalizeAtomicOperations,
91
+ };
@@ -1,101 +1,101 @@
1
- 'use strict';
2
-
3
- // MCP-RAC — grammatica computer-use v0.1 (spec §6.4).
4
- //
5
- // Un'azione e' un blocco:
6
- //
7
- // ACTION <observation_id> ttl=<ms>
8
- // <dx> <dy> <scroll> ; K1 ; K2 ; ... ; K6 END
9
- //
10
- // Il primo segmento e' il MOVIMENTO (relativo, distribuito sull'orizzonte);
11
- // i successivi sono KEYSET: l'insieme dei tasti tenuti premuti per 33 ms.
12
- // `_` e' il keyset vuoto (tutto rilasciato). Le chiavi assenti si rilasciano.
13
- //
14
- // La grammatica e' volutamente povera: niente Enter, niente Canc, niente tasto
15
- // Windows. Non e' una dimenticanza — e' il primo strato di sicurezza (spec §13):
16
- // le azioni irreversibili non devono essere ESPRIMIBILI nel canale reattivo,
17
- // prima ancora che vietate da una policy.
18
-
19
- const LIMITI = Object.freeze({
20
- MOUSE_DELTA: 1000, // dx, dy in [-1000, 1000]
21
- SCROLL: 5, // scroll in [-5, 5]
22
- TASTI_PER_CHUNK: 4, // spec §13: "max quattro tasti/chunk"
23
- CHUNK_PER_AZIONE: 6, // 6 keyset da 33 ms = orizzonte 200 ms
24
- TTL_MASSIMO_MS: 250, // oltre la lease il chunk non ha senso
25
- CHUNK_MS: 33, // durata nominale di un keyset
26
- ORIZZONTE_MS: 200,
27
- BUFFER_MASSIMO: 16 * 1024,
28
- });
29
-
30
- // L'alfabeto completo dei tasti ammessi. Un token fuori da qui non e' "un tasto
31
- // sconosciuto da inoltrare": e' un errore di grammatica che avvelena l'azione.
32
- const TASTI_INTERNI = (() => {
33
- const out = new Set(['Shift', 'Ctrl', 'Alt', 'Space', 'Tab', 'Esc', 'LB', 'RB', 'MB']);
34
- for (let c = 65; c <= 90; c += 1) out.add(String.fromCharCode(c)); // A..Z
35
- for (let d = 0; d <= 9; d += 1) out.add(String(d)); // 0..9
36
- return out;
37
- })();
38
- // Un Set congelato resta modificabile tramite add/delete. Esponiamo quindi una
39
- // fotografia realmente immutabile e teniamo il Set usato dalla validazione
40
- // chiuso nel modulo.
41
- const TASTI = Object.freeze([...TASTI_INTERNI].sort());
42
-
43
- function interoIn(testo, min, max) {
44
- if (!/^-?\d+$/.test(testo)) return null;
45
- const n = Number(testo);
46
- return n >= min && n <= max ? n : null;
47
- }
48
-
49
- // "ACTION 184 ttl=250" -> { observationId, ttlMs } | { errore }
50
- function analizzaIntestazione(riga) {
51
- const m = /^ACTION\s+(\d+)\s+ttl=(\d+)$/.exec(String(riga || '').trim());
52
- if (!m) return { errore: 'intestazione malformata' };
53
- const observationNumber = Number(m[1]);
54
- const ttlMs = Number(m[2]);
55
- if (!Number.isSafeInteger(observationNumber)) return { errore: 'observation_id fuori limite' };
56
- if (ttlMs <= 0 || ttlMs > LIMITI.TTL_MASSIMO_MS) {
57
- return { errore: `ttl fuori limite (1..${LIMITI.TTL_MASSIMO_MS})` };
58
- }
59
- return { observationId: m[1], ttlMs };
60
- }
61
-
62
- // "92 0 0" -> { tipo: 'mouse', dx, dy, scroll } | { errore }
63
- function analizzaMouse(segmento) {
64
- const parti = String(segmento || '').trim().split(/\s+/);
65
- if (parti.length !== 3) return { errore: 'il movimento vuole esattamente dx dy scroll' };
66
- const dx = interoIn(parti[0], -LIMITI.MOUSE_DELTA, LIMITI.MOUSE_DELTA);
67
- const dy = interoIn(parti[1], -LIMITI.MOUSE_DELTA, LIMITI.MOUSE_DELTA);
68
- const scroll = interoIn(parti[2], -LIMITI.SCROLL, LIMITI.SCROLL);
69
- if (dx === null || dy === null || scroll === null) return { errore: 'movimento fuori range' };
70
- return { tipo: 'mouse', dx, dy, scroll };
71
- }
72
-
73
- // "Shift W" | "_" -> { tipo: 'keyset', tasti: [...] } | { errore }
74
- // I tasti si normalizzano ordinati e senza duplicati: {W,Shift} e {Shift,W}
75
- // sono lo stesso stato fisico, e l'esecutore confronta insiemi.
76
- function analizzaKeyset(segmento) {
77
- const testo = String(segmento || '').trim();
78
- if (testo === '_') return { tipo: 'keyset', tasti: [] };
79
- if (!testo) return { errore: 'keyset vuoto: usa _ per rilasciare tutti i tasti' };
80
- const token = testo.split(/\s+/);
81
- if (token.length > LIMITI.TASTI_PER_CHUNK) {
82
- return { errore: `piu' di ${LIMITI.TASTI_PER_CHUNK} tasti in un chunk` };
83
- }
84
- const visti = new Set();
85
- for (const t of token) {
86
- if (!TASTI_INTERNI.has(t)) return { errore: `tasto fuori alfabeto: ${t}` };
87
- visti.add(t);
88
- }
89
- return { tipo: 'keyset', tasti: [...visti].sort() };
90
- }
91
-
92
- // Serializzazione, per il simulatore e i replay: da azione strutturata al testo
93
- // che il decoder emetterebbe. Round-trip garantito dai test.
94
- function serializza(azione) {
95
- const testa = `ACTION ${azione.observationId} ttl=${azione.ttlMs}\n`;
96
- const mouse = `${azione.mouse.dx} ${azione.mouse.dy} ${azione.mouse.scroll}`;
97
- const keyset = azione.keyset.map((k) => (k.length ? k.join(' ') : '_'));
98
- return testa + [mouse, ...keyset].join(' ; ') + ' ;\nEND';
99
- }
100
-
101
- module.exports = { LIMITI, TASTI, analizzaIntestazione, analizzaMouse, analizzaKeyset, serializza };
1
+ 'use strict';
2
+
3
+ // MCP-RAC — grammatica computer-use v0.1 (spec §6.4).
4
+ //
5
+ // Un'azione e' un blocco:
6
+ //
7
+ // ACTION <observation_id> ttl=<ms>
8
+ // <dx> <dy> <scroll> ; K1 ; K2 ; ... ; K6 END
9
+ //
10
+ // Il primo segmento e' il MOVIMENTO (relativo, distribuito sull'orizzonte);
11
+ // i successivi sono KEYSET: l'insieme dei tasti tenuti premuti per 33 ms.
12
+ // `_` e' il keyset vuoto (tutto rilasciato). Le chiavi assenti si rilasciano.
13
+ //
14
+ // La grammatica e' volutamente povera: niente Enter, niente Canc, niente tasto
15
+ // Windows. Non e' una dimenticanza — e' il primo strato di sicurezza (spec §13):
16
+ // le azioni irreversibili non devono essere ESPRIMIBILI nel canale reattivo,
17
+ // prima ancora che vietate da una policy.
18
+
19
+ const LIMITI = Object.freeze({
20
+ MOUSE_DELTA: 1000, // dx, dy in [-1000, 1000]
21
+ SCROLL: 5, // scroll in [-5, 5]
22
+ TASTI_PER_CHUNK: 4, // spec §13: "max quattro tasti/chunk"
23
+ CHUNK_PER_AZIONE: 6, // 6 keyset da 33 ms = orizzonte 200 ms
24
+ TTL_MASSIMO_MS: 250, // oltre la lease il chunk non ha senso
25
+ CHUNK_MS: 33, // durata nominale di un keyset
26
+ ORIZZONTE_MS: 200,
27
+ BUFFER_MASSIMO: 16 * 1024,
28
+ });
29
+
30
+ // L'alfabeto completo dei tasti ammessi. Un token fuori da qui non e' "un tasto
31
+ // sconosciuto da inoltrare": e' un errore di grammatica che avvelena l'azione.
32
+ const TASTI_INTERNI = (() => {
33
+ const out = new Set(['Shift', 'Ctrl', 'Alt', 'Space', 'Tab', 'Esc', 'LB', 'RB', 'MB']);
34
+ for (let c = 65; c <= 90; c += 1) out.add(String.fromCharCode(c)); // A..Z
35
+ for (let d = 0; d <= 9; d += 1) out.add(String(d)); // 0..9
36
+ return out;
37
+ })();
38
+ // Un Set congelato resta modificabile tramite add/delete. Esponiamo quindi una
39
+ // fotografia realmente immutabile e teniamo il Set usato dalla validazione
40
+ // chiuso nel modulo.
41
+ const TASTI = Object.freeze([...TASTI_INTERNI].sort());
42
+
43
+ function interoIn(testo, min, max) {
44
+ if (!/^-?\d+$/.test(testo)) return null;
45
+ const n = Number(testo);
46
+ return n >= min && n <= max ? n : null;
47
+ }
48
+
49
+ // "ACTION 184 ttl=250" -> { observationId, ttlMs } | { errore }
50
+ function analizzaIntestazione(riga) {
51
+ const m = /^ACTION\s+(\d+)\s+ttl=(\d+)$/.exec(String(riga || '').trim());
52
+ if (!m) return { errore: 'intestazione malformata' };
53
+ const observationNumber = Number(m[1]);
54
+ const ttlMs = Number(m[2]);
55
+ if (!Number.isSafeInteger(observationNumber)) return { errore: 'observation_id fuori limite' };
56
+ if (ttlMs <= 0 || ttlMs > LIMITI.TTL_MASSIMO_MS) {
57
+ return { errore: `ttl fuori limite (1..${LIMITI.TTL_MASSIMO_MS})` };
58
+ }
59
+ return { observationId: m[1], ttlMs };
60
+ }
61
+
62
+ // "92 0 0" -> { tipo: 'mouse', dx, dy, scroll } | { errore }
63
+ function analizzaMouse(segmento) {
64
+ const parti = String(segmento || '').trim().split(/\s+/);
65
+ if (parti.length !== 3) return { errore: 'il movimento vuole esattamente dx dy scroll' };
66
+ const dx = interoIn(parti[0], -LIMITI.MOUSE_DELTA, LIMITI.MOUSE_DELTA);
67
+ const dy = interoIn(parti[1], -LIMITI.MOUSE_DELTA, LIMITI.MOUSE_DELTA);
68
+ const scroll = interoIn(parti[2], -LIMITI.SCROLL, LIMITI.SCROLL);
69
+ if (dx === null || dy === null || scroll === null) return { errore: 'movimento fuori range' };
70
+ return { tipo: 'mouse', dx, dy, scroll };
71
+ }
72
+
73
+ // "Shift W" | "_" -> { tipo: 'keyset', tasti: [...] } | { errore }
74
+ // I tasti si normalizzano ordinati e senza duplicati: {W,Shift} e {Shift,W}
75
+ // sono lo stesso stato fisico, e l'esecutore confronta insiemi.
76
+ function analizzaKeyset(segmento) {
77
+ const testo = String(segmento || '').trim();
78
+ if (testo === '_') return { tipo: 'keyset', tasti: [] };
79
+ if (!testo) return { errore: 'keyset vuoto: usa _ per rilasciare tutti i tasti' };
80
+ const token = testo.split(/\s+/);
81
+ if (token.length > LIMITI.TASTI_PER_CHUNK) {
82
+ return { errore: `piu' di ${LIMITI.TASTI_PER_CHUNK} tasti in un chunk` };
83
+ }
84
+ const visti = new Set();
85
+ for (const t of token) {
86
+ if (!TASTI_INTERNI.has(t)) return { errore: `tasto fuori alfabeto: ${t}` };
87
+ visti.add(t);
88
+ }
89
+ return { tipo: 'keyset', tasti: [...visti].sort() };
90
+ }
91
+
92
+ // Serializzazione, per il simulatore e i replay: da azione strutturata al testo
93
+ // che il decoder emetterebbe. Round-trip garantito dai test.
94
+ function serializza(azione) {
95
+ const testa = `ACTION ${azione.observationId} ttl=${azione.ttlMs}\n`;
96
+ const mouse = `${azione.mouse.dx} ${azione.mouse.dy} ${azione.mouse.scroll}`;
97
+ const keyset = azione.keyset.map((k) => (k.length ? k.join(' ') : '_'));
98
+ return testa + [mouse, ...keyset].join(' ; ') + ' ;\nEND';
99
+ }
100
+
101
+ module.exports = { LIMITI, TASTI, analizzaIntestazione, analizzaMouse, analizzaKeyset, serializza };