@mosaicoo/svg-engine 0.1.0

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.
@@ -0,0 +1,1834 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injector, Signal, InjectionToken, Provider } from '@angular/core';
3
+ import { Disposable, NodeId } from '@mosaicoo/svg-engine/core';
4
+ import { MenuContributionRegistry, MenuContribution, EditorPlugin } from '@mosaicoo/svg-engine/edit';
5
+
6
+ /**
7
+ * **NLU (Natural Language Understanding) types — D-046 Fase 1**
8
+ *
9
+ * Rule-based natural-language → command pipeline. Zero ML, zero
10
+ * external download — pure regex + dictionary + Levenshtein fuzzy
11
+ * matching. Cobre ~70–80% dos comandos comuns ("undo", "delete",
12
+ * "criar retângulo vermelho"). Fase 2 (ML classifier) e Fase 3 (SLM)
13
+ * são entry points separados que reúsam esse mesmo contrato.
14
+ *
15
+ * **Entry point separado `svg-engine/ai/nlu`** (não dentro de `edit`)
16
+ * pra que a camada AI fique 100% desacoplada — Modo 1 (headless puro
17
+ * D-037) não importa nada de `ai/` quando não usar. Fase 2 e 3
18
+ * (Transformers.js / WebLLM) entram em `svg-engine/ai/nlu-ml` e
19
+ * `svg-engine/ai/nlu-slm`, ambos reaproveitando o contrato
20
+ * {@link NluIntent} / {@link NaturalLanguageService} definido aqui.
21
+ *
22
+ * **Por que mirrors `MenuContributionContext`**: o NLU é uma surface
23
+ * a mais (`<svge-menu-bar>`, `<svge-toolbar>`, `<svge-context-menu>`
24
+ * + agora command palette por voz/texto). Mantém-se o mesmo
25
+ * contrato multi-editor scope (D-042/D-043): `injector` na ctx,
26
+ * services resolvidos lazy do scope ativo.
27
+ */
28
+ /**
29
+ * Per-fire context passed to {@link NluIntent.execute} (and to the
30
+ * NLU service `parse` / `execute` entry points). Espelho exato do
31
+ * {@link MenuContributionContext} pra que handlers reaproveitem a
32
+ * mesma resolução de scope ativo (per-editor no D-042).
33
+ */
34
+ interface NluContext {
35
+ /**
36
+ * Injector do consumer (UI component que disparou o parse — command
37
+ * palette, voice input, etc). Em apps route-scoped, é o injector
38
+ * do editor ativo; em single-editor é equivalente ao root.
39
+ */
40
+ readonly injector: Injector;
41
+ }
42
+ /**
43
+ * Tipo declarativo de slot que um intent pode extrair do input.
44
+ *
45
+ * **Variantes**:
46
+ * - `number`: número decimal/inteiro (com unidade opcional como `px`).
47
+ * - `color`: nome de cor (PT/EN via dicionário) OU hex `#rrggbb` /
48
+ * `rgb()` / `hsl()`, com suporte a **intensificadores adjacentes**
49
+ * ("azul claro", "verde bem escuro") via `parseColorPhrase`.
50
+ * - `shape`: nome de forma (PT/EN via `SHAPE_DICTIONARY`) — resolve
51
+ * automaticamente "círculo"→`circle`, "retângulo"→`rect`,
52
+ * "balão"→`group`, etc. **Use este em vez de `enum` quando o
53
+ * slot for forma SVG** — `enum` exige match exato do canonical
54
+ * ('rect'), não suporta vocabulário PT/EN nem aliases semânticos.
55
+ * - `enum`: valor de uma lista fechada (lookup case-insensitive,
56
+ * com fuzzy match dist ≤ 1). Use para domínios fechados sem
57
+ * vocabulário multilíngue (e.g., 'landscape'/'portrait').
58
+ * - `string`: token livre — usado raramente, intent precisa ser
59
+ * tolerante a ruído.
60
+ *
61
+ * **`optional`**: quando `false` (default), confidence cai abaixo do
62
+ * threshold se o slot não for preenchido. Quando `true`, o slot é
63
+ * preenchido com `default` (ou `undefined`) e confidence mantém.
64
+ */
65
+ type NluSlotSchema = {
66
+ readonly kind: 'number';
67
+ readonly optional?: boolean;
68
+ readonly default?: number;
69
+ readonly anchorKeywords?: readonly string[];
70
+ /**
71
+ * Quando `false`, o slot **não** é preenchido pelo pass posicional
72
+ * genérico — só por pre-passes específicos (ex.: `count` da
73
+ * repetição, extraído como "número antes de uma forma"). Evita que
74
+ * um número de dimensão seja capturado como contagem por engano.
75
+ * Default `true` (positional).
76
+ */
77
+ readonly positional?: boolean;
78
+ } | {
79
+ readonly kind: 'color';
80
+ readonly optional?: boolean;
81
+ readonly default?: string;
82
+ readonly anchorKeywords?: readonly string[];
83
+ } | {
84
+ readonly kind: 'shape';
85
+ readonly optional?: boolean;
86
+ readonly default?: string;
87
+ readonly anchorKeywords?: readonly string[];
88
+ } | {
89
+ readonly kind: 'enum';
90
+ readonly values: readonly string[];
91
+ readonly optional?: boolean;
92
+ readonly default?: string;
93
+ readonly anchorKeywords?: readonly string[];
94
+ /**
95
+ * Quando `false`, o match do enum é **exato** (apenas
96
+ * `values.includes(token)`), sem fuzzy Levenshtein. Use para
97
+ * vocabulários curtos em que o fuzzy geraria falso-positivo
98
+ * (ex.: layout `'grade'` casaria `'grande'` por distância 1).
99
+ * Default `true` (fuzzy ligado).
100
+ */
101
+ readonly fuzzy?: boolean;
102
+ } | {
103
+ readonly kind: 'string';
104
+ readonly optional?: boolean;
105
+ readonly default?: string;
106
+ readonly anchorKeywords?: readonly string[];
107
+ } | {
108
+ /**
109
+ * **`point`** — extrai par `{ x, y }` de **dois números adjacentes**
110
+ * (ex: "100 50" ou "100x50"). Combinar com `anchorKeywords`
111
+ * (`['posicao','position','em','at']`) pra desambiguar de outros
112
+ * slots numéricos no mesmo intent.
113
+ */
114
+ readonly kind: 'point';
115
+ readonly optional?: boolean;
116
+ readonly default?: {
117
+ readonly x: number;
118
+ readonly y: number;
119
+ };
120
+ readonly anchorKeywords?: readonly string[];
121
+ } | {
122
+ /**
123
+ * **`gradient`** — preenchimento por gradiente. Extraído **só** por
124
+ * um pre-pass dedicado e **só quando a palavra-chave** ("gradiente"/
125
+ * "degradê"/"degrade"/"gradient") aparece — assim cores sólidas
126
+ * ("amarelo", "vermelho") seguem 100% intactas. O valor extraído é
127
+ * `{ kind: 'linear'|'radial', direction: 'horizontal'|'vertical'|
128
+ * 'diagonal', colors: string[] }`: o handler deriva os stops (1 cor
129
+ * → clara→escura; N cores → distribuídas) e a geometria. Nunca é
130
+ * posicional (não compete com o slot `fill`).
131
+ */
132
+ readonly kind: 'gradient';
133
+ readonly optional?: boolean;
134
+ /** Não usado (gradient é só pre-pass) — presente p/ uniformidade do union. */
135
+ readonly anchorKeywords?: readonly string[];
136
+ };
137
+ /**
138
+ * **`anchorKeywords`** — palavras que precedem o valor do slot no
139
+ * input ("**borda** azul" → slot `stroke=azul`; "**posição** 100 50"
140
+ * → slot `position={x:100,y:50}`).
141
+ *
142
+ * Como funciona:
143
+ * 1. Extractor faz primeiro um **pass anchored**: pra cada slot com
144
+ * `anchorKeywords`, procura o anchor token (exato ou fuzzy ≤1) e
145
+ * consome o(s) próximo(s) token(s) compatível(eis) com o `kind`.
146
+ * 2. Depois faz o pass **posicional** normal pros slots sem anchor.
147
+ * 3. Tokens já consumidos no anchored pass ficam de fora do posicional
148
+ * — evita dupla atribuição.
149
+ *
150
+ * Útil quando o mesmo intent tem múltiplos slots de mesmo `kind`
151
+ * (e.g., `create-shape` com `fill` + `stroke` ambos `kind: 'color'`):
152
+ * sem âncora, o extractor pega a primeira cor pra `fill` e ignora a
153
+ * segunda. Com âncora `'borda'/'contorno'/'stroke'`, "fill vermelho
154
+ * borda azul" produz `{fill:'red', stroke:'blue'}` corretamente.
155
+ */
156
+ /**
157
+ * Definição de um intent registrável no {@link NaturalLanguageService}.
158
+ *
159
+ * **Filosofia**:
160
+ * - `keywords`: palavras-chave **disparadoras** (PT/EN). Match exato
161
+ * (após tokenize/deacento) vira anchor da intent — se nenhuma
162
+ * keyword aparecer (mesmo aproximada via Levenshtein), o intent
163
+ * nem é considerado candidato.
164
+ * - `slots`: opcionais; cada um declara `kind` (number/color/enum/string)
165
+ * e se é `optional`. Extractor preenche o que conseguir.
166
+ * - `execute`: handler que recebe slots já extraídos + ctx. MUST NOT
167
+ * throw — falhas devem ser logged + ignored (assim como
168
+ * `MenuContribution.run`).
169
+ *
170
+ * **Multi-editor (D-042/D-043)**: `execute` recebe `NluContext` e
171
+ * deve resolver services do `ctx.injector` — não capturar root
172
+ * services em closures.
173
+ *
174
+ * **Exemplo**:
175
+ * ```ts
176
+ * nlu.registerIntent({
177
+ * id: 'create-rect',
178
+ * keywords: ['rectangle', 'rect', 'retângulo', 'retangulo'],
179
+ * actionKeywords: ['create', 'add', 'criar', 'desenhar'],
180
+ * slots: {
181
+ * fill: { kind: 'color', optional: true },
182
+ * width: { kind: 'number', optional: true, default: 100 },
183
+ * height: { kind: 'number', optional: true, default: 100 },
184
+ * },
185
+ * execute(slots, ctx) {
186
+ * const bus = ctx.injector.get(CommandBus);
187
+ * bus.dispatch(new InsertNodeCommand(rootId, createRect({...}, {style: {fill: slots.fill}})));
188
+ * },
189
+ * });
190
+ * ```
191
+ */
192
+ interface NluIntent {
193
+ /** Stable unique id. Reverse-DNS recommended (`svge.builtin.nlu.create-rect`). */
194
+ readonly id: string;
195
+ /**
196
+ * Palavras-chave **primárias** que identificam o intent (substantivos,
197
+ * objetos: "rectangle", "retângulo", "circle", "círculo", "selection").
198
+ * Pelo menos uma deve aparecer (exato ou fuzzy ≤ 2 dist) pra intent
199
+ * ser candidato. Comparação após `tokenize` (lowercase + deacento).
200
+ */
201
+ readonly keywords: readonly string[];
202
+ /**
203
+ * **`requiredAllGroups`** (D-046 review-7) — strict-AND matching:
204
+ * cada **grupo** representa um elemento semântico do intent que
205
+ * DEVE estar presente no input (pelo menos uma palavra do grupo
206
+ * casa, exato ou fuzzy). Diferente de `keywords` (OR fraco),
207
+ * requiredAllGroups exige TODOS os grupos preenchidos.
208
+ *
209
+ * **Caso de uso típico**: intents auto-descobertos de menu items
210
+ * multi-token como "Select All" precisam garantir que TANTO o
211
+ * verbo (select / selecionar / selecione) QUANTO o qualificador
212
+ * (all / tudo / todos) apareçam — senão "selecione estrela"
213
+ * matcharia "Select All" e selecionaria tudo erradamente.
214
+ *
215
+ * **Estrutura**: array de grupos; cada grupo é array de variantes
216
+ * sinônimas pra uma posição semântica. Exemplo "Select All":
217
+ * ```
218
+ * requiredAllGroups: [
219
+ * ['select', 'selecionar', 'selecione', 'marcar', ...], // verbo
220
+ * ['all', 'tudo', 'todos', 'todas', 'everything'], // qualificador
221
+ * ]
222
+ * ```
223
+ *
224
+ * Quando `requiredAllGroups` é declarado, o `keywords` ainda é
225
+ * usado pra ranking de confidence mas o **gate** de candidato vira
226
+ * o requiredAllGroups (todos satisfeitos OR keywords match com ≥1
227
+ * candidato — política pragmática).
228
+ */
229
+ readonly requiredAllGroups?: readonly (readonly string[])[];
230
+ /**
231
+ * Verbos / ações associadas (opcional): "create", "criar", "add",
232
+ * "desenhar", "delete", "deletar". Quando presente, eleva confidence
233
+ * mas não é obrigatório — alguns intents são triggados só pelo
234
+ * substantivo ("rectangle" sozinho pode criar um). Útil pra
235
+ * desambiguar intents que compartilham keywords (e.g.,
236
+ * "delete circle" vs "create circle").
237
+ */
238
+ readonly actionKeywords?: readonly string[];
239
+ /** Schema dos slots extraíveis (por nome). */
240
+ readonly slots?: Record<string, NluSlotSchema>;
241
+ /**
242
+ * Marca o intent como **destrutivo** — UI deve pedir confirmação
243
+ * antes de executar (mesmo confidence alta). Ex: delete, clear,
244
+ * reset all. Default `false`.
245
+ */
246
+ readonly destructive?: boolean;
247
+ /** Hint humano pra UI exibir como sugestão / autocomplete. */
248
+ readonly description?: string;
249
+ /** Handler executado quando o intent é o top candidate + acima do threshold. */
250
+ execute(slots: Record<string, unknown>, ctx: NluContext): void | Promise<void>;
251
+ }
252
+ /**
253
+ * Resultado de {@link NaturalLanguageService.parse} — um candidato
254
+ * que casou com o input, com confidence + slots extraídos. Ordenado
255
+ * por confidence desc.
256
+ */
257
+ interface NluCandidate {
258
+ /** Intent que casou. */
259
+ readonly intent: NluIntent;
260
+ /**
261
+ * Confidence em `[0, 1]`.
262
+ * - `≥ 0.7`: executa direto.
263
+ * - `0.4–0.7`: pede confirmação (UI decide via threshold configurável).
264
+ * - `< 0.4`: rejeita (parse retorna candidate mesmo assim — UI usa
265
+ * pra sugerir alternativas).
266
+ */
267
+ readonly confidence: number;
268
+ /** Slots extraídos do input (preenche `default` quando ausente). */
269
+ readonly slots: Record<string, unknown>;
270
+ /**
271
+ * Razões que levaram ao score — útil pra debug + UI explicar
272
+ * "achei isso porque casou 'criar' (exato) e 'retângulo' (fuzzy)".
273
+ */
274
+ readonly matches: readonly NluMatchReason[];
275
+ }
276
+ /** Detalhe de um match individual contribuindo pro confidence. */
277
+ interface NluMatchReason {
278
+ /** O que casou: keyword, actionKeyword, ou slot. */
279
+ readonly kind: 'keyword' | 'action' | 'slot';
280
+ /** O termo da intent que foi matched. */
281
+ readonly term: string;
282
+ /** O token do input que casou (após tokenize). */
283
+ readonly token: string;
284
+ /** Distância de Levenshtein (0 = exato). */
285
+ readonly distance: number;
286
+ /**
287
+ * Índice do token no array de input (D-046 review-10).
288
+ *
289
+ * Quando `kind === 'slot'`, é `-1` (slot value não é necessariamente
290
+ * um único token — pode ter sido extraído de uma frase de cor ou
291
+ * `kind: 'point'` compondo 2 números).
292
+ *
293
+ * Para `kind: 'keyword'` ou `'action'`, é o índice exato do token
294
+ * que casou. Usado pelo extractor pra marcar consumed e evitar
295
+ * dupla atribuição (resolve bug de `tokens.indexOf(value)` retornando
296
+ * sempre 1ª ocorrência em inputs com tokens repetidos).
297
+ */
298
+ readonly tokenIndex?: number;
299
+ }
300
+ /**
301
+ * Threshold semântico do parse. UI consumers podem customizar via
302
+ * `parse(text, ctx, { threshold: 0.5 })`.
303
+ */
304
+ interface NluParseOptions {
305
+ /** Mínimo confidence pra candidate ser retornado. Default `0.3`. */
306
+ readonly threshold?: number;
307
+ /** Máximo de candidates retornados (ordenados por confidence). Default `5`. */
308
+ readonly maxResults?: number;
309
+ }
310
+ /**
311
+ * Threshold semântico do execute. UI consumers podem customizar.
312
+ */
313
+ interface NluExecuteOptions extends NluParseOptions {
314
+ /**
315
+ * Confidence mínima pra dispatchar direto. Default `0.7`.
316
+ * Abaixo disso, o `confirmGate` decide.
317
+ */
318
+ readonly autoExecuteThreshold?: number;
319
+ /**
320
+ * Hook opcional pra confirmação interativa (e.g., mostrar dialog
321
+ * com "Você quis dizer: criar retângulo?"). Recebe o top candidate
322
+ * e retorna se deve executar.
323
+ *
324
+ * **Default**: `null` — sem confirmação, executa qualquer
325
+ * candidate ≥ `autoExecuteThreshold`, rejeita os abaixo.
326
+ *
327
+ * **Destrutivos**: quando `intent.destructive === true`,
328
+ * `confirmGate` é **obrigatório** — sem ele, intents destrutivos
329
+ * são sempre rejeitados (defesa contra dispatch acidental de
330
+ * delete/clear via fuzzy match ruim).
331
+ */
332
+ readonly confirmGate?: ((candidate: NluCandidate) => boolean | Promise<boolean>) | null;
333
+ }
334
+ /**
335
+ * Resultado final de {@link NaturalLanguageService.execute}.
336
+ */
337
+ interface NluExecuteResult {
338
+ /** `true` se um candidate foi executado, `false` se rejeitado / abaixo do threshold / sem match. */
339
+ readonly executed: boolean;
340
+ /** O candidate executado (ou top candidate quando `executed === false`, pra UI exibir alternativas). */
341
+ readonly candidate: NluCandidate | null;
342
+ /**
343
+ * Lista de candidates considerados (top N por confidence). Útil
344
+ * pra UI mostrar "também achei: ...".
345
+ */
346
+ readonly alternatives: readonly NluCandidate[];
347
+ /** Por que não executou (`null` quando `executed === true`). */
348
+ readonly rejection: 'no-match' | 'below-threshold' | 'confirmation-declined' | 'destructive-no-gate' | 'execute-error' | null;
349
+ /**
350
+ * Erro capturado quando `rejection === 'execute-error'` (D-046
351
+ * review-10 / M4). Service envolve `intent.execute()` em try/catch
352
+ * — handler que lança não derruba a UI nem deixa Promise pendurada.
353
+ */
354
+ readonly error?: Error;
355
+ }
356
+
357
+ /**
358
+ * **`NaturalLanguageService`** — D-046 Fase 1 (rule-based NLU).
359
+ *
360
+ * Singleton root-provided que registra {@link NluIntent}s e
361
+ * traduz texto livre em comandos dispatcháveis. Composição direta
362
+ * dos parsers (`tokenize` → `fuzzyMatch` → `extractSlots`) com
363
+ * scoring de confidence.
364
+ *
365
+ * **Algoritmo de `parse(text)`**:
366
+ *
367
+ * 1. **Tokenize**: lowercase + deacento + split (preserva números/hex).
368
+ * 2. **Para cada intent registrado**, calcula `score`:
369
+ * a. **Keywords**: fuzzy match com adaptive distance. Se nenhuma
370
+ * keyword bate (nem aproximada), pula a intent. Score base do
371
+ * melhor match.
372
+ * b. **Action keywords** (opcional): match contra `actionKeywords`
373
+ * da intent + lookup canonical no `ACTION_DICTIONARY`. Eleva
374
+ * score quando casa.
375
+ * c. **Slots**: extrai com `extractSlots`. Slots obrigatórios não
376
+ * preenchidos penalizam (subtrai 0.15 cada). Slots opcionais
377
+ * preenchidos elevam levemente (0.05 cada).
378
+ * d. **Penalidade por distância**: cada match fuzzy não-exato
379
+ * reduz o score proporcionalmente.
380
+ * 3. **Sort + filter** por threshold + cap em `maxResults`.
381
+ *
382
+ * **`execute(text)`**: parse + auto-execute se ≥ `autoExecuteThreshold`
383
+ * (default 0.7), respeitando `confirmGate` pra destrutivos.
384
+ *
385
+ * **Multi-editor (D-042/D-043)**: o `NluContext` recebido pelos
386
+ * handlers tem `injector` do consumer — services devem ser resolvidos
387
+ * dele, nunca cacheados em closure.
388
+ *
389
+ * **Por que `Injectable({ providedIn: 'root' })`**: o registry de
390
+ * intents é global (todos os editors compartilham a definição), mas
391
+ * a EXECUÇÃO é per-scope via `NluContext.injector`. Mesmo padrão
392
+ * que `MenuContributionRegistry`.
393
+ */
394
+ declare class NaturalLanguageService {
395
+ private readonly _intents;
396
+ /** Snapshot reativo dos intents registrados (insertion order). */
397
+ readonly intents: Signal<readonly NluIntent[]>;
398
+ /**
399
+ * **D-046 review-10 (L12)**: counter conveniente — consumers que só
400
+ * precisam do número (e.g., status bar "26 intents disponíveis")
401
+ * subscrevem aqui em vez do array inteiro (evita re-render quando
402
+ * o array muda mas length não).
403
+ */
404
+ readonly intentsCount: Signal<number>;
405
+ /**
406
+ * **Cache de description tokens por intent** — usado pelo
407
+ * description-matching pass (meio-termo "semantic disambiguator"
408
+ * SEM ML deps). Computado lazy quando o intent aparece pela 1ª vez
409
+ * num scoring run; invalidado quando o intent é desregistrado.
410
+ *
411
+ * **Por que cache**: tokenize + filter stopwords é ~10ms por
412
+ * description, e o parse roda em cada keystroke do usuário —
413
+ * computar 30+ intents toda vez ficaria perceptível.
414
+ */
415
+ private readonly descriptionTokensCache;
416
+ /**
417
+ * Registra um intent novo. Retorna `Disposable` pra remover (em geral
418
+ * trackeada pelo plugin via `ctx.track()` igual aos outros registries).
419
+ *
420
+ * **Erros**:
421
+ * - Throw em `id` vazio
422
+ * - Throw em `id` duplicado
423
+ * - Throw em `keywords` vazio (intent sem keyword nunca seria matched)
424
+ */
425
+ registerIntent(intent: NluIntent): Disposable;
426
+ /**
427
+ * Tokeniza + filtra description do intent e armazena no cache.
428
+ * No-op se o intent não tem description ou já está cached.
429
+ */
430
+ private populateDescriptionCache;
431
+ /**
432
+ * **Description-matching boost** (meio-termo "semantic disambiguator"
433
+ * sem ML deps).
434
+ *
435
+ * Tokeniza a `description` do intent (sem stopwords) e conta hits
436
+ * vs tokens do input. Cada hit adiciona +0.04 (cap 0.2 total). Não
437
+ * substitui keyword matching — só complementa quando há candidatos
438
+ * com score próximo (e.g., dois intents com keyword exata, ambos
439
+ * 0.65; o de description mais alinhada vence).
440
+ *
441
+ * **Cache**: tokens da description são computados na 1ª chamada
442
+ * e guardados em `descriptionTokensCache` keyed por `intent.id`.
443
+ *
444
+ * **Quando contribui**: SEMPRE — score é monotônico. UI consumers
445
+ * que queiram desligar podem passar `options.disableDescriptionBoost`
446
+ * (não implementado ainda — Fase 2 quando ML chegar e a heurística
447
+ * for mais relevante).
448
+ */
449
+ private descriptionBoost;
450
+ /** Procura intent por id (`null` se ausente). */
451
+ getIntent(id: string): NluIntent | null;
452
+ /**
453
+ * Parse `text` em candidates ordenados por confidence desc.
454
+ *
455
+ * @param text input natural do usuário
456
+ * @param _ctx NluContext (não usado no parse — só no execute; aqui
457
+ * mantido por simetria de API)
458
+ * @param options threshold + maxResults
459
+ */
460
+ parse(text: string, _ctx: NluContext, options?: NluParseOptions): readonly NluCandidate[];
461
+ /**
462
+ * Parse + dispatch o top candidate.
463
+ *
464
+ * **Política**:
465
+ * - `intent.destructive === true` SEM `confirmGate` → rejeita
466
+ * (defesa contra delete acidental).
467
+ * - `confidence ≥ autoExecuteThreshold` (default 0.7) → executa direto
468
+ * (ou via confirmGate quando destrutivo).
469
+ * - `confidence < autoExecuteThreshold` E `confirmGate` presente →
470
+ * chama o gate; só executa se retornar `true`.
471
+ * - Senão → rejeita com `'below-threshold'`.
472
+ */
473
+ execute(text: string, ctx: NluContext, options?: NluExecuteOptions): Promise<NluExecuteResult>;
474
+ /**
475
+ * Conectores que separam comandos numa frase composta
476
+ * ("crie X, e um Y"). Split em `,`/`;` + " e "/" depois "/" também ".
477
+ */
478
+ private static readonly CLAUSE_SPLIT_RE;
479
+ /**
480
+ * Verbos de comando (criação/edição) que marcam uma cláusula como
481
+ * comando autônomo. Deaccentuados/lowercase (forma que o tokenizer gera).
482
+ */
483
+ private static readonly COMMAND_VERBS;
484
+ /**
485
+ * **Multi-comando por frase** — divide o texto nos conectores e executa
486
+ * cada cláusula como um comando independente. Cada `execute()` despacha
487
+ * seu próprio command no bus, então **cada forma é um passo de undo
488
+ * separado**.
489
+ *
490
+ * **Sem regressão**: quando não há split real (frase simples, ou os
491
+ * "pedaços" são apenas fragmentos — listas de cor "preto e branco",
492
+ * números "100 e 200"), recai EXATAMENTE no `execute(text)` de sempre.
493
+ *
494
+ * Heurística anti over-split: um pedaço só vira comando separado se
495
+ * contiver uma **forma** ou um **verbo de comando**; senão é re-fundido
496
+ * ao anterior.
497
+ *
498
+ * @returns um {@link NluExecuteResult} por comando, na ordem da frase.
499
+ */
500
+ executeSequence(text: string, ctx: NluContext, options?: NluExecuteOptions): Promise<readonly NluExecuteResult[]>;
501
+ /**
502
+ * Divide `text` em cláusulas-comando. Pedaços que não parecem comando
503
+ * (sem forma nem verbo) são re-fundidos ao anterior. Retorna ≤1 item
504
+ * quando não há split real.
505
+ */
506
+ private splitIntoClauses;
507
+ /** `true` se a cláusula contém uma forma ou um verbo de comando. */
508
+ private clauseLooksLikeCommand;
509
+ /**
510
+ * Executa um candidate **específico** (escolhido pelo usuário via UI
511
+ * — e.g., clique numa alternativa) aplicando as mesmas regras de
512
+ * segurança do {@link execute}: destructive sem gate rejeita;
513
+ * confidence baixa sem gate rejeita.
514
+ *
515
+ * **Por que precisa de método separado**: chamar `candidate.intent.execute()`
516
+ * direto bypassa toda a lógica de threshold/destructive. UI components
517
+ * (`<svge-nlu-input>` alternatives list) DEVEM usar este método em
518
+ * vez de `intent.execute()` direto, pra preservar a defesa contra
519
+ * delete acidental.
520
+ *
521
+ * **Política idêntica ao `execute`**:
522
+ * - Destrutivo SEM `confirmGate` → rejeita `'destructive-no-gate'`.
523
+ * - Destrutivo COM gate → roda gate; só executa se aprovar.
524
+ * - Não-destrutivo com confidence ≥ `autoExecuteThreshold` → executa.
525
+ * - Não-destrutivo com confidence < threshold E gate presente →
526
+ * roda gate.
527
+ * - Senão → `'below-threshold'`.
528
+ *
529
+ * @param candidate o NluCandidate a executar (vindo de `parse()`).
530
+ * @param ctx contexto (injector do consumer).
531
+ * @param options threshold + confirmGate.
532
+ */
533
+ executeCandidate(candidate: NluCandidate, ctx: NluContext, options?: NluExecuteOptions): Promise<NluExecuteResult>;
534
+ static ɵfac: i0.ɵɵFactoryDeclaration<NaturalLanguageService, never>;
535
+ static ɵprov: i0.ɵɵInjectableDeclaration<NaturalLanguageService>;
536
+ }
537
+
538
+ /**
539
+ * **`discoverMenuIntents`** — D-046 Fase 1.
540
+ *
541
+ * Auto-promove TODAS as contribuições do {@link MenuContributionRegistry}
542
+ * em intents NLU. Cada menu item vira um intent cujo handler executa
543
+ * o `run()` original com o `MenuContributionContext` derivado do
544
+ * `NluContext.injector`.
545
+ *
546
+ * **Como derivamos keywords a partir do `label`**: tokenize o label
547
+ * (lowercase + deacento), remove stopwords, mantém o resto. "Bring
548
+ * to Front" → `['bring', 'front']`; "Selecionar tudo" →
549
+ * `['selecionar', 'tudo']`. Plugins que queiram aliases adicionais
550
+ * podem registrar intents customizados via
551
+ * {@link NaturalLanguageService.registerIntent} sem conflito.
552
+ *
553
+ * **Skip de dividers + items sem label + roadmap**: `divider: true`,
554
+ * label vazio, ou `comingSoon: true` (D-085) não produzem intent — não
555
+ * são ações executáveis (o `run()` de um item roadmap é no-op).
556
+ *
557
+ * **Destrutivos**: items cujo label contém "delete"/"remove"/"clear"/
558
+ * "deletar"/"excluir"/"remover"/"apagar" são marcados `destructive: true`
559
+ * — a NLU exige `confirmGate` pra dispatch automático.
560
+ *
561
+ * **Multi-editor (D-042/D-043)**: o handler propaga `ctx.injector`
562
+ * para o `MenuContributionContext` esperado pelo `run()` do menu
563
+ * contribution. Cadeia de scope ativo preservada end-to-end.
564
+ *
565
+ * **Reactivity**: este helper é one-shot — registra os intents que
566
+ * EXISTEM no momento da chamada. Para auto-discovery contínuo
567
+ * (plugins instalados depois também viram intents),
568
+ * use {@link discoverMenuIntentsReactive} — wrapper baseado em
569
+ * `effect()` que reflete mudanças do registry em tempo real
570
+ * (Audit #12).
571
+ *
572
+ * **Retorno**: array de `Disposable` (um por intent registrado) +
573
+ * count. O `composedDispose` permite cleanup em massa via
574
+ * `ctx.track()`.
575
+ */
576
+ interface DiscoverMenuIntentsResult {
577
+ /** Disposables dos intents criados — array vazio quando nada foi descoberto. */
578
+ readonly disposables: readonly Disposable[];
579
+ /** Quantos intents foram efetivamente criados. */
580
+ readonly count: number;
581
+ /** Disposable composto que dispara dispose() em todos. */
582
+ readonly composedDispose: Disposable;
583
+ }
584
+ /**
585
+ * Cria intent NLU a partir de uma `MenuContribution` específica.
586
+ * Retorna `null` quando a contribution não é elegível (divider, label
587
+ * vazio, ou tokens insuficientes pra match útil).
588
+ */
589
+ declare function menuContributionToIntent(contrib: MenuContribution): NluIntent | null;
590
+ /**
591
+ * Walks o registry, cria intents pra cada contribution elegível, e
592
+ * registra todos no service. Skip silencioso pra contribuições que
593
+ * gerariam intent duplicado (caller pode já ter registrado um
594
+ * customizado com o mesmo id) — não throws.
595
+ */
596
+ declare function discoverMenuIntents(registry: MenuContributionRegistry, service: NaturalLanguageService): DiscoverMenuIntentsResult;
597
+ /**
598
+ * **`discoverMenuIntentsReactive`** — Audit #12. Reactive companion to
599
+ * {@link discoverMenuIntents}.
600
+ *
601
+ * Performs an **immediate synchronous discovery** of all current menu
602
+ * contributions (so callers reading `service.intents()` right after
603
+ * this returns see the auto-discovered intents — same observable
604
+ * behavior as the one-shot helper at install time), AND registers an
605
+ * Angular `effect()` that re-runs discovery whenever
606
+ * `registry.contributions()` emits — covering plugins installed AFTER
607
+ * the NLU plugin and contributions disposed at any later time.
608
+ *
609
+ * **Strategy on each change**: tear down the previous batch via its
610
+ * `composedDispose`, then rebuild a fresh batch from the new registry
611
+ * snapshot. Coarse-grained but correct: no diff/merge bookkeeping
612
+ * means no chance of half-state on race conditions, at the cost of
613
+ * O(N) work per registry mutation (N is small for menu items, usually
614
+ * ≤ 50). Custom intents the consumer registered out-of-band stay
615
+ * untouched because `discoverMenuIntents` skips ids already present.
616
+ *
617
+ * **Why an effect, not a manual subscription**: Angular signals don't
618
+ * expose a `subscribe()` — `effect()` IS the official subscription
619
+ * mechanism. It also gets disposed cleanly via `effectRef.destroy()`,
620
+ * letting the returned `Disposable` cover both the effect AND the
621
+ * current batch of intents.
622
+ *
623
+ * **Injection context requirement**: `effect()` may only be created
624
+ * inside an injection context. We accept an explicit `Injector` and
625
+ * use `runInInjectionContext` so the function works from anywhere —
626
+ * including plugin `install(ctx)` blocks that aren't injection
627
+ * contexts themselves.
628
+ *
629
+ * **Echo skipping via reference identity**: Angular schedules the
630
+ * effect's first run for a later microtask, NOT immediately at
631
+ * creation. That first run might happen BEFORE or AFTER unrelated
632
+ * signal mutations. Using a "skip first" flag is unreliable. Instead
633
+ * we capture the exact array reference that the initial sync
634
+ * discovery consumed and short-circuit any firing where the signal
635
+ * still returns that same reference — guaranteed safe because
636
+ * `MenuContributionRegistry.register` and the returned dispose both
637
+ * produce a NEW array (immutable update), so identity comparison
638
+ * cleanly distinguishes "no real change" from "actual mutation".
639
+ */
640
+ interface DiscoverMenuIntentsReactiveResult {
641
+ /**
642
+ * Composite disposable — stops the effect AND tears down the
643
+ * currently-tracked batch of intents. Idempotent: calling
644
+ * `dispose()` twice is a silent no-op.
645
+ */
646
+ readonly disposable: Disposable;
647
+ }
648
+ declare function discoverMenuIntentsReactive(registry: MenuContributionRegistry, service: NaturalLanguageService, injector: Injector): DiscoverMenuIntentsReactiveResult;
649
+
650
+ declare const builtinNluPlugin: EditorPlugin;
651
+
652
+ /**
653
+ * Tokenizer multilíngue (PT + EN) — D-046 Fase 1.
654
+ *
655
+ * Operações em ordem:
656
+ * 1. Lowercase
657
+ * 2. **Deacentuação** via `String.prototype.normalize('NFD')` + strip
658
+ * de marks combining — "vermelho" → "vermelho", "círculo" →
659
+ * "circulo", "ação" → "acao". Permite o dicionário ser keyed em
660
+ * ASCII puro e ainda casar com input acentuado.
661
+ * 3. Split por whitespace + pontuação comum, **preservando**:
662
+ * - números (incluindo decimais `1.5`, `2,5` — pt/en)
663
+ * - dimensões compostas `100x50`
664
+ * - hex colors `#ff0000`
665
+ * 4. Filter de tokens vazios.
666
+ *
667
+ * **Não filtra stopwords aqui** — algumas etapas (slot-extractor)
668
+ * precisam dos tokens originais pra ordem posicional. Caller decide
669
+ * quando aplicar `isStopword`.
670
+ *
671
+ * **Por que não usar Intl.Segmenter / NLP libs**: zero-deps é
672
+ * requisito explícito da Fase 1 (D-046). Tokenização "ingênua"
673
+ * cobre o vocabulário de comandos de editor de SVG sem ruído.
674
+ */
675
+ /**
676
+ * Normaliza um texto removendo acentos via NFD + strip de marks
677
+ * combining (Unicode category `Mn`).
678
+ *
679
+ * **Por que NFD**: separa o caractere base da marca combinante
680
+ * (`á` → `a` + ` ́ `), depois removemos só as marcas. Funciona pra PT
681
+ * (acentos agudos, til, cedilha) e qualquer alfabeto latino estendido.
682
+ *
683
+ * **Edge case** mantido: caracteres sem decomposição NFD passam
684
+ * intactos (preserva tokens em outros alfabetos se aparecerem).
685
+ */
686
+ declare function deaccent(input: string): string;
687
+ /**
688
+ * Normaliza texto: lowercase + deacentuação. Não tokeniza — só
689
+ * normaliza pra usar em dicionários, comparações.
690
+ */
691
+ declare function normalize(input: string): string;
692
+ /**
693
+ * Tokeniza um input textual seguindo a pipeline descrita no header.
694
+ *
695
+ * **Algoritmo (single-pass scanner)**:
696
+ * 1. Itera caractere por caractere.
697
+ * 2. Caractere de pontuação (`PUNCT_CHARS`) OU `,`/`.` que NÃO está
698
+ * entre dígitos → fecha o token corrente.
699
+ * 3. Caractere "normal" (incluindo `,` / `.` entre dígitos) → acumula.
700
+ * 4. Fim do input → fecha o último token se houver.
701
+ *
702
+ * **Retorno**: array de tokens normalizados (lowercase, sem acento),
703
+ * sem vazios, preservando ordem de aparição (importante pro
704
+ * slot-extractor encontrar "vermelho" depois de "fill").
705
+ *
706
+ * **Custo**: O(n) no tamanho do input.
707
+ */
708
+ declare function tokenize(input: string): readonly string[];
709
+ /**
710
+ * Tokeniza E remove stopwords. Atalho conveniente; quando você
711
+ * precisa preservar a ordem original (slot extraction posicional),
712
+ * use `tokenize()` direto.
713
+ */
714
+ declare function tokenizeWithoutStopwords(input: string, stopwords: ReadonlySet<string>): readonly string[];
715
+
716
+ /**
717
+ * Distância de Levenshtein — edit distance clássica (insertion,
718
+ * deletion, substitution; cada uma custa 1). Usada pelo fuzzy
719
+ * matcher pra encontrar a palavra mais próxima no dicionário quando
720
+ * o usuário digita errado ("vermelo" → "vermelho", dist 1;
721
+ * "retangulo" → "retangulo", dist 0).
722
+ *
723
+ * **Implementação**: 2-row DP (memória O(min(m,n))), curto-circuita
724
+ * cedo via "early termination on max distance" — útil porque o
725
+ * matcher fuzzy só aceita distâncias pequenas (≤ 2 default) e abandonar
726
+ * cedo evita varrer a tabela inteira pra strings totalmente diferentes.
727
+ *
728
+ * **Sem cache**: as strings são curtas (tokens de palavras, ≤ 20
729
+ * chars) e o overhead de hashmap supera o custo de recomputar. Se
730
+ * benchmarks futuros mostrarem hot spot, adicionar cache LRU
731
+ * Map<`${a}|${b}`, number> com cap pequeno.
732
+ *
733
+ * **Zero deps** (requisito Fase 1 D-046).
734
+ */
735
+ /**
736
+ * Calcula a distância de Levenshtein entre `a` e `b`.
737
+ *
738
+ * @param a primeira string (lowercase + deacentuada já preferível)
739
+ * @param b segunda string
740
+ * @param maxDist se informado, retorna `Infinity` assim que prova
741
+ * que a distância real excede `maxDist` (early
742
+ * termination, ~10× speedup em mismatches grandes).
743
+ * @returns distância em [0, max(a.length, b.length)] ou `Infinity`
744
+ * se `maxDist` foi excedida.
745
+ */
746
+ declare function levenshtein(a: string, b: string, maxDist?: number): number;
747
+ /**
748
+ * Encontra o melhor match em `candidates` pra `query`, retornando
749
+ * a string + distância. Curto-circuita em match exato.
750
+ *
751
+ * @param query termo a buscar (normalizado)
752
+ * @param candidates lista de strings candidatas (normalizadas)
753
+ * @param maxDist distância máxima aceitável; ignora candidates além disso
754
+ * @returns `{ match, distance }` ou `null` se nenhum candidate dentro do limite
755
+ */
756
+ declare function bestMatch(query: string, candidates: readonly string[], maxDist: number): {
757
+ match: string;
758
+ distance: number;
759
+ } | null;
760
+
761
+ /**
762
+ * Resultado de um match fuzzy entre um token de input e um termo
763
+ * de dicionário/keyword.
764
+ */
765
+ interface FuzzyMatch {
766
+ /** Termo do dicionário que casou. */
767
+ readonly term: string;
768
+ /** Token do input (preservado pra `NluMatchReason`). */
769
+ readonly token: string;
770
+ /** Distância de Levenshtein (0 = exato). */
771
+ readonly distance: number;
772
+ /**
773
+ * Confidence sub-1 derivada da distância vs comprimento do termo.
774
+ * Match exato = 1.0; match com 1 erro em palavra de 8 letras ~0.875.
775
+ * Match com 2 erros em palavra de 4 letras = 0.5.
776
+ */
777
+ readonly score: number;
778
+ /**
779
+ * **`tokenIndex`** (D-046 review-10): índice do token original no
780
+ * array de entrada. Sempre populado por {@link fuzzyMatchAny} e
781
+ * {@link fuzzyMatchAll}; em {@link fuzzyMatchToken} fica `-1` porque
782
+ * o caller não passa array (e sabe o índice por contexto próprio).
783
+ *
784
+ * **Motivação**: `NaturalLanguageService` precisa marcar o ÍNDICE
785
+ * do token consumido (não só seu valor string) para evitar bug onde
786
+ * `tokens.indexOf(value)` retorna sempre a 1ª ocorrência — quebrando
787
+ * em inputs com tokens repetidos ("vermelho borda vermelho").
788
+ */
789
+ readonly tokenIndex: number;
790
+ }
791
+ /**
792
+ * Política de distância adaptativa por tamanho do termo:
793
+ * - Termos ≤ 3 chars: distância máxima 0 (matches exatos só — "rect",
794
+ * "red" são tão curtas que 1 typo já vira outra palavra).
795
+ * - Termos 4–6 chars: distância máxima 1.
796
+ * - Termos ≥ 7 chars: distância máxima 2.
797
+ *
798
+ * Cap absoluto: 2 (acima disso é palavra diferente, não typo).
799
+ */
800
+ declare function adaptiveMaxDistance(termLength: number): number;
801
+ /**
802
+ * Tenta casar um único token de input contra uma lista de termos
803
+ * (keys de dicionário OU keywords de intent), respeitando distância
804
+ * adaptativa por tamanho do termo.
805
+ *
806
+ * **Retorno**: melhor match (menor distância) ou `null` se nenhum
807
+ * dentro do orçamento de distância adaptativo.
808
+ *
809
+ * **Custo**: O(N) onde N = `terms.length`, com early termination
810
+ * herdada do `levenshtein()`.
811
+ */
812
+ declare function fuzzyMatchToken(token: string, terms: readonly string[]): FuzzyMatch | null;
813
+ /**
814
+ * Match fuzzy de **qualquer** token de uma lista contra **qualquer**
815
+ * termo de uma lista. Retorna o melhor casamento (ou `null`).
816
+ *
817
+ * Útil pra "alguma das keywords da intent aparece no input?". Pára
818
+ * cedo no primeiro match exato (distância 0).
819
+ */
820
+ declare function fuzzyMatchAny(tokens: readonly string[], terms: readonly string[]): FuzzyMatch | null;
821
+ /**
822
+ * Versão "all matches" — retorna **todos** os matches fuzzy de um
823
+ * token contra a lista, sem early-termination. Útil quando precisamos
824
+ * de ranking completo (intent registry com muitos candidates).
825
+ */
826
+ declare function fuzzyMatchAll(token: string, terms: readonly string[], maxDistOverride?: number): readonly FuzzyMatch[];
827
+
828
+ /**
829
+ * Resultado da extração: valores por nome de slot. Slots não
830
+ * encontrados são `undefined` (caller decide se aplica default).
831
+ */
832
+ type ExtractedSlots = Record<string, unknown>;
833
+ /**
834
+ * Indica quais tokens foram consumidos por matchings (keywords/
835
+ * actions/slots) — útil pra evitar dupla atribuição (token "100"
836
+ * não deve preencher dois slots `number` distintos).
837
+ */
838
+ interface ExtractContext {
839
+ /** Set mutável de índices de tokens já consumidos. */
840
+ readonly consumedIndices: Set<number>;
841
+ }
842
+ /**
843
+ * Parse um token como número. Aceita "100", "1.5", "1,5", "100px".
844
+ * Trata `,` como separador decimal (PT). Não aceita signos relativos
845
+ * exóticos. Retorna `null` quando não é número.
846
+ */
847
+ declare function parseNumberToken(token: string): number | null;
848
+ /**
849
+ * Tenta resolver um token ISOLADO como cor: hex direto, rgb/hsl
850
+ * funcional OU lookup no dicionário com fuzzy match (dist ≤ 1 pra
851
+ * typos como "vermelo"). Para frases com intensificador adjacente
852
+ * ("azul claro"), use {@link parseColorPhrase}.
853
+ */
854
+ declare function parseColorToken(token: string): string | null;
855
+ /**
856
+ * Resultado de {@link parseColorPhrase}: cor resolvida + quantos
857
+ * tokens adjacentes foram consumidos (sempre ≥ 1).
858
+ */
859
+ interface ColorPhraseMatch {
860
+ /** Cor resolvida (`#rrggbb`, CSS keyword, ou outro do dicionário). */
861
+ readonly color: string;
862
+ /**
863
+ * Quantos tokens (a partir de `startIdx`) compõem a frase de cor.
864
+ * Sempre ≥ 1. Caller deve marcar todos esses índices como consumed.
865
+ *
866
+ * Exemplos:
867
+ * - "azul" → tokensConsumed = 1
868
+ * - "azul claro" → tokensConsumed = 2 (intensificador adjacente)
869
+ * - "bem azul escuro" → tokensConsumed = 3 (multiplier + cor + mod)
870
+ * - "muito claro azul" → tokensConsumed = 3 (multiplier + mod ANTES da cor)
871
+ */
872
+ readonly tokensConsumed: number;
873
+ }
874
+ /**
875
+ * Resolve uma cor a partir de `tokens[startIdx]`, considerando
876
+ * **intensificadores adjacentes** que modificam o lightness do hex
877
+ * base. Suporta padrões:
878
+ *
879
+ * - `[modifier] cor [modifier]` — "azul claro" / "claro azul" /
880
+ * "verde escuro" / "dark green"
881
+ * - `[multiplier] [modifier] cor` — "muito claro azul" / "very
882
+ * dark red"
883
+ * - `cor [multiplier] [modifier]` — "verde bem escuro" / "blue
884
+ * really dark"
885
+ *
886
+ * Não modifica `none` / `currentColor` / `inherit` (sem hex base).
887
+ *
888
+ * Retorna `null` quando `tokens[startIdx]` não é cor nem
889
+ * intensificador-seguido-de-cor.
890
+ */
891
+ declare function parseColorPhrase(tokens: readonly string[], startIdx: number): ColorPhraseMatch | null;
892
+ /**
893
+ * Extrai dimensão composta `100x50` em `{ width, height }` quando
894
+ * presente. Retorna `null` quando o token não é dimensão.
895
+ */
896
+ declare function parseDimensionToken(token: string): {
897
+ width: number;
898
+ height: number;
899
+ } | null;
900
+ /**
901
+ * Extrai os slots declarados em `schemas` a partir dos `tokens`,
902
+ * marcando consumed indices no `ctx`.
903
+ *
904
+ * **Algoritmo (single pass por slot, ordem declarada)**:
905
+ * 1. Para cada slot, varre tokens ainda não consumidos.
906
+ * 2. Tenta resolver de acordo com o `kind`.
907
+ * 3. Se encontrar, marca consumed + atribui ao slot.
908
+ * 4. Aplica `default` quando não encontrou + `optional: true`.
909
+ *
910
+ * **Edge case `kind: 'number'`** com dimensão `100x50`: se houver
911
+ * slot `width` E `height` no schema (nessa ordem), o token de
912
+ * dimensão preenche os dois e consome o índice uma vez. Caso
913
+ * contrário, só o primeiro number do par é usado.
914
+ *
915
+ * **Edge case `kind: 'color'`** com intensificador adjacente
916
+ * ("azul claro"): {@link parseColorPhrase} consome todos os
917
+ * tokens da frase de cor — caller marca múltiplos consumed indices.
918
+ */
919
+ declare function extractSlots(tokens: readonly string[], schemas: Record<string, NluSlotSchema>, ctx?: ExtractContext): ExtractedSlots;
920
+
921
+ /**
922
+ * Color parsing helpers — D-046 Fase 1 enrich.
923
+ *
924
+ * Reconhece formatos CSS além de hex puro:
925
+ * - `rgb(255, 0, 0)` / `rgba(255, 0, 0, 0.5)`
926
+ * - `hsl(0, 100%, 50%)` / `hsla(0, 100%, 50%, 0.5)`
927
+ * - Hex (3, 4, 6, 8 dígitos) — delegado pro caller via `HEX_COLOR_RE`
928
+ *
929
+ * Também expõe `lightenHex` / `darkenHex` usados pelos intensificadores
930
+ * ("azul claro" / "verde escuro" / "bem escuro") no slot extractor.
931
+ *
932
+ * **Por que arquivo separado** (em vez de empilhar tudo no
933
+ * slot-extractor): coesão. Color math é responsabilidade única;
934
+ * tornar reaproveitável por outros parsers / plugins / UI components.
935
+ *
936
+ * **Sem dependências externas** (requisito Fase 1).
937
+ */
938
+ /** Hex 3, 4, 6 ou 8 dígitos (com `#`). */
939
+ declare const HEX_COLOR_RE: RegExp;
940
+ /**
941
+ * Parse `rgb(...)` / `rgba(...)` → hex (ignora alpha por ora, sem
942
+ * suporte a alpha no slot color). Retorna `null` se não casar.
943
+ */
944
+ declare function parseRgbFunction(input: string): string | null;
945
+ /**
946
+ * Parse `hsl(...)` / `hsla(...)` → hex. Retorna `null` se não casar.
947
+ */
948
+ declare function parseHslFunction(input: string): string | null;
949
+ /**
950
+ * Vocabulário de intensificadores que **modificam** uma cor adjacente.
951
+ * `delta` é a variação de lightness (HSL, 0..1) aplicada ao hex base.
952
+ * Positivo = mais claro, negativo = mais escuro.
953
+ *
954
+ * Multiplicador "bem" / "muito" / "very" / "really" dobra o efeito do
955
+ * intensificador subsequente (vide `LIGHTNESS_MULTIPLIERS`).
956
+ */
957
+ declare const LIGHTNESS_MODIFIERS: Readonly<Record<string, number>>;
958
+ /**
959
+ * Multiplicadores que amplificam o intensificador SEGUINTE.
960
+ * "bem escuro" / "muito claro" / "very dark" → multiplica o delta.
961
+ */
962
+ declare const LIGHTNESS_MULTIPLIERS: Readonly<Record<string, number>>;
963
+ /**
964
+ * Aplica delta de lightness a um hex `#rrggbb` (ou `#rgb`). Mantém
965
+ * matiz e saturação; só altera L no espaço HSL.
966
+ */
967
+ declare function adjustHexLightness(hex: string, delta: number): string;
968
+ /** Conveniência: clarear. */
969
+ declare function lightenHex(hex: string, amount?: number): string;
970
+ /** Conveniência: escurecer. */
971
+ declare function darkenHex(hex: string, amount?: number): string;
972
+ /**
973
+ * `#rrggbb` (ou `#rgb`) → `[r, g, b]` em [0, 255]. `null` se inválido.
974
+ * Ignora alpha quando 4 ou 8 dígitos.
975
+ */
976
+ declare function hexToRgb(hex: string): [number, number, number] | null;
977
+ /** RGB [0,255] → HSL [h:0-360, s:0-1, l:0-1]. */
978
+ declare function rgbToHsl(r: number, g: number, b: number): [number, number, number];
979
+ /** HSL [h:0-360, s:0-1, l:0-1] → RGB [0,255]. */
980
+ declare function hslToRgb(h: number, s: number, l: number): [number, number, number];
981
+
982
+ /**
983
+ * **Color dictionary — English (EN).**
984
+ *
985
+ * Kept separate from PT so idiomatic / regional variants can be reviewed
986
+ * independently. Final dict consumed by the parser
987
+ * (`COLOR_DICTIONARY` in `colors.ts`) merges PT + EN.
988
+ *
989
+ * **Key convention**: lowercase, no diacritics (tokenizer applies
990
+ * `deaccent()` before lookup). Don't duplicate with PT — when a token
991
+ * is the same in both (e.g., CSS keywords like `'royalblue'`), keep
992
+ * it ONLY here.
993
+ *
994
+ * **Why split by language**:
995
+ * - Audit: spot vocabulary gaps PT vs EN without scanning 200 lines.
996
+ * - Maintenance: extend EN dict without touching PT.
997
+ * - Language detection (`detectLanguage()` in `language-detect.ts`)
998
+ * uses per-dict hit counts to estimate the dominant input language.
999
+ */
1000
+ declare const COLOR_DICTIONARY_EN: Readonly<Record<string, string>>;
1001
+
1002
+ /**
1003
+ * **Dicionário de cores — Português (PT-BR/PT-PT).**
1004
+ *
1005
+ * Mantido **separado de EN** pra que extensões idiomáticas / regionais
1006
+ * (variantes brasileiras vs portuguesas, gírias, neologismos) possam
1007
+ * ser revisadas isoladamente. O dict final consumido pelo parser
1008
+ * (`COLOR_DICTIONARY` em `colors.ts`) faz merge de PT + EN.
1009
+ *
1010
+ * **Convenção de chave**: lowercase + SEM acento (o tokenizer aplica
1011
+ * `deaccent()` antes da busca). NÃO duplicar com EN — se a palavra é a
1012
+ * mesma nos dois idiomas (e.g., `'royalblue'` é universal CSS), coloque
1013
+ * SÓ em `colors-en.ts`.
1014
+ *
1015
+ * **Por que separar por idioma**:
1016
+ * - Auditoria: ver lacunas de vocabulário PT vs EN sem ler 200 linhas.
1017
+ * - Manutenção: adicionar variante regional sem tocar EN.
1018
+ * - Detecção de idioma (`detectLanguage()` em `language-detect.ts`)
1019
+ * usa contagem de hits por dict pra estimar idioma dominante.
1020
+ */
1021
+ declare const COLOR_DICTIONARY_PT: Readonly<Record<string, string>>;
1022
+
1023
+ /**
1024
+ * **Merged dict** — PT + EN. EN tem precedência em colisão (mas como
1025
+ * convencionamos não duplicar, colisões só acontecem em CSS keywords
1026
+ * universais — comportamento idempotente).
1027
+ */
1028
+ declare const COLOR_DICTIONARY: Readonly<Record<string, string>>;
1029
+ /**
1030
+ * Lista de keys (lowercase, sem acento) — usada pelo fuzzy matcher
1031
+ * pra propor cores próximas quando o usuário digita errado
1032
+ * ("vermelo" → "vermelho").
1033
+ */
1034
+ declare const COLOR_KEYS: readonly string[];
1035
+ /**
1036
+ * Resolve um nome de cor (lowercased, deacentuado) para hex/CSS.
1037
+ * Retorna `null` quando o nome não consta no dicionário.
1038
+ *
1039
+ * **Multi-idioma**: consulta o merged dict (PT + EN), então funciona
1040
+ * pra qualquer input independente do idioma do usuário.
1041
+ */
1042
+ declare function resolveColorName(name: string): string | null;
1043
+
1044
+ /**
1045
+ * **NLU-specific shape vocabulary** — separado do `ShapeKind` em
1046
+ * `svg-engine/edit/lib/tool` (que tem `'rect' | 'ellipse' | 'polygon'`
1047
+ * pras shape tools de drawing). O NLU precisa de uma lista maior
1048
+ * porque mapeia vocabulário falado/escrito, não capability de tool.
1049
+ *
1050
+ * **Polígonos específicos** (D-046 review-5): triangle/pentagon/
1051
+ * hexagon/octagon/rhombus/star são kinds próprios para que o handler
1052
+ * `create-shape` saiba **quantos lados** gerar sem precisar inferir
1053
+ * do input. Antes, "triangulo" e "hexagono" caíam em `'polygon'`
1054
+ * genérico e o handler não tinha info pra desenhar — virava no-op.
1055
+ */
1056
+ type NluShapeKind = 'rect' | 'ellipse' | 'circle' | 'line' | 'path' | 'triangle' | 'rhombus' | 'pentagon' | 'hexagon' | 'octagon' | 'star' | 'polygon' | 'polyline' | 'text' | 'image' | 'group' | 'svg';
1057
+
1058
+ /**
1059
+ * **Shape dictionary — English.**
1060
+ *
1061
+ * Maps EN vocabulary (technical names + common UX semantic aliases)
1062
+ * to {@link NluShapeKind} canonical. Kept separate from PT for clarity.
1063
+ *
1064
+ * **Key convention**: lowercase, no diacritics.
1065
+ */
1066
+
1067
+ declare const SHAPE_DICTIONARY_EN: Readonly<Record<string, NluShapeKind>>;
1068
+
1069
+ /**
1070
+ * **Dicionário de formas — Português.**
1071
+ *
1072
+ * Mapeia vocabulário PT (nomes técnicos + semanticos UX) pra
1073
+ * {@link NluShapeKind} canonical. Separado de EN pra cobrir variantes
1074
+ * regionais isoladamente. Final merged em `shapes.ts`.
1075
+ *
1076
+ * **Convenção de chave**: lowercase + SEM acento.
1077
+ */
1078
+
1079
+ declare const SHAPE_DICTIONARY_PT: Readonly<Record<string, NluShapeKind>>;
1080
+
1081
+ /**
1082
+ * Dicionário de formas — **PT + EN merged** → canonical shape kind.
1083
+ *
1084
+ * O kind canonical mapeia para os factories existentes em
1085
+ * `svg-engine/core` (`createRect`, `createEllipse`, `createPath`,
1086
+ * `createLine`, `createPolygon`, `createPolyline`, `createText`,
1087
+ * `createImage`, `createGroup`). Plugins built-in usam esse mapeamento
1088
+ * pra resolver o slot `shape` (e.g., "criar retângulo" → `rect` →
1089
+ * `createRect`).
1090
+ *
1091
+ * **Convenções de chaves**:
1092
+ * - **lowercase + SEM acento** (tokenizer faz `deaccent()` antes).
1093
+ * - PT/EN vivem em arquivos separados (`shapes-pt.ts`, `shapes-en.ts`).
1094
+ * - Quando uma palavra é idêntica nos dois idiomas, fica APENAS em EN.
1095
+ * - **Semantic aliases** ("nó" → circle, "conector" → line, "balão"
1096
+ * → group) mapeiam vocabulário UX para shapes existentes.
1097
+ *
1098
+ * **Geometria por nome**: "estrela" mapeia para `'star'` com geometria
1099
+ * real (5 pontas via `regularStarPoints`); "coração" cai em `'star'`
1100
+ * como fallback semântico até ganhar geometria própria. Outros ícones
1101
+ * nomeados ainda fora do vocabulário ficam para a fase que integrar
1102
+ * uma icon library (Fase 1.x).
1103
+ */
1104
+
1105
+ declare const SHAPE_DICTIONARY: Readonly<Record<string, NluShapeKind>>;
1106
+ /** Keys disponíveis pro fuzzy matcher. */
1107
+ declare const SHAPE_KEYS: readonly string[];
1108
+ /**
1109
+ * Resolve um nome de forma (lowercased + deacentuado) para kind.
1110
+ * Retorna `null` quando não é uma forma conhecida.
1111
+ *
1112
+ * **Multi-idioma**: consulta o merged dict (PT + EN).
1113
+ */
1114
+ declare function resolveShapeKind(name: string): NluShapeKind | null;
1115
+
1116
+ /**
1117
+ * **Action canonicals** — vocabulário fechado de verbos suportados.
1118
+ *
1119
+ * Cada termo PT ou EN é mapeado pra um canonical aqui. Plugins NLU
1120
+ * declaram `actionKeywords: ['create', 'delete']` (canonicals) em
1121
+ * vez de listar todas as conjugações — o `resolveActionCanonical(token)`
1122
+ * faz a tradução transparente.
1123
+ *
1124
+ * **Por que canonical em vez de só strings**:
1125
+ * - TypeScript valida cobertura nos consumers (intent não pode
1126
+ * declarar action que não existe).
1127
+ * - Refactor de vocabulário sem quebrar intents (renomear `'paint'`
1128
+ * pra `'fill'` é mudança LOCAL aqui + nas tabelas).
1129
+ *
1130
+ * **Adicionar canonical novo**: 1) adiciona aqui, 2) adiciona entradas
1131
+ * em `actions-pt.ts` E `actions-en.ts`, 3) consumer plugin declara em
1132
+ * `intent.actionKeywords`. Sem step 3 o canonical é "registrado mas
1133
+ * não usado" — não quebra, mas é code-smell.
1134
+ */
1135
+ type ActionCanonical = 'create' | 'delete' | 'select' | 'select-all' | 'deselect' | 'group' | 'ungroup' | 'undo' | 'redo' | 'zoom-in' | 'zoom-out' | 'zoom-reset' | 'copy' | 'paste' | 'cut' | 'duplicate' | 'move' | 'rotate' | 'resize' | 'flip' | 'align' | 'distribute' | 'bring-forward' | 'send-backward' | 'show' | 'hide' | 'toggle' | 'union' | 'intersect' | 'subtract' | 'exclude' | 'divide' | 'convert' | 'lock' | 'unlock' | 'export' | 'import';
1136
+
1137
+ /**
1138
+ * **Action dictionary — English.**
1139
+ *
1140
+ * Maps EN verbs (base form + common synonyms) to the canonical token
1141
+ * shared with PT. Keep entries minimal and synonyms commonly seen in
1142
+ * UI / spoken English; avoid archaic forms.
1143
+ *
1144
+ * **Key convention**: lowercase, no diacritics. Same canonical as PT
1145
+ * (verified by `ActionCanonical` type).
1146
+ */
1147
+
1148
+ declare const ACTION_DICTIONARY_EN: Readonly<Record<string, ActionCanonical>>;
1149
+
1150
+ /**
1151
+ * **Dicionário de ações — Português.**
1152
+ *
1153
+ * Mapeia conjugações PT comuns (infinitivo + imperativo afirmativo)
1154
+ * para o termo canonical (`'create'`, `'delete'`, etc) compartilhado
1155
+ * com EN. Mantido separado pra cobrir variações regionais sem ruído.
1156
+ *
1157
+ * **Convenção de chave**: lowercase + SEM acento.
1158
+ * **Cobertura mínima**: pra cada verbo, infinitivo (`mover`) +
1159
+ * imperativo formal (`mova`) + imperativo informal (`movimente`).
1160
+ */
1161
+
1162
+ declare const ACTION_DICTIONARY_PT: Readonly<Record<string, ActionCanonical>>;
1163
+
1164
+ /**
1165
+ * **Merged dict** — PT + EN. EN tem precedência em colisão (mas
1166
+ * convencionamos não duplicar — colisões só acontecem em CSS keywords).
1167
+ */
1168
+ declare const ACTION_DICTIONARY: Readonly<Record<string, ActionCanonical>>;
1169
+ /** Keys disponíveis pro fuzzy matcher. */
1170
+ declare const ACTION_KEYS: readonly string[];
1171
+ /**
1172
+ * Resolve uma palavra (lowercased + deacentuada) para ação canonical.
1173
+ * Retorna `null` quando não é verbo conhecido.
1174
+ *
1175
+ * **Multi-idioma**: consulta o merged dict (PT + EN), então funciona
1176
+ * pra qualquer input independente do idioma do usuário.
1177
+ */
1178
+ declare function resolveActionCanonical(word: string): ActionCanonical | null;
1179
+
1180
+ /**
1181
+ * **Stopwords — English.**
1182
+ *
1183
+ * Low-information words ignored by the parser to avoid noise in
1184
+ * keyword/action/slot matching. Includes articles, prepositions,
1185
+ * conjunctions, pronouns, conversational fillers and weak modal verbs.
1186
+ *
1187
+ * Does NOT include action verbs ("create", "delete") — those live in
1188
+ * `actions-en.ts`. Nor colors/shapes (those are slot values).
1189
+ *
1190
+ * **Key convention**: lowercase, no diacritics.
1191
+ */
1192
+ declare const STOPWORDS_EN: ReadonlySet<string>;
1193
+
1194
+ /**
1195
+ * **Stopwords — Português.**
1196
+ *
1197
+ * Palavras de baixa informação ignoradas pelo parser pra evitar ruído
1198
+ * no matching de keywords/actions/slots. Inclui artigos, preposições,
1199
+ * conjunções, pronomes, fillers conversacionais e verbos auxiliares
1200
+ * fracos que aparecem em comandos coloquiais.
1201
+ *
1202
+ * **Não inclui** verbos de ação ("criar", "deletar"); esses estão em
1203
+ * `actions-pt.ts`. Nem cores/formas (esses são slot values).
1204
+ *
1205
+ * **Convenção de chave**: lowercase + SEM acento (consistente com
1206
+ * o tokenizer — `deaccent()` é aplicado antes do lookup).
1207
+ */
1208
+ declare const STOPWORDS_PT: ReadonlySet<string>;
1209
+
1210
+ /** **Merged set** — PT ∪ EN. */
1211
+ declare const STOPWORDS: ReadonlySet<string>;
1212
+ /**
1213
+ * `true` se o token é uma stopword (case-insensitive, sem acento).
1214
+ * O caller é responsável por já ter tokenizado/normalizado.
1215
+ *
1216
+ * **Multi-idioma**: cobre PT + EN no mesmo set.
1217
+ */
1218
+ declare function isStopword(token: string): boolean;
1219
+
1220
+ /** Idiomas suportados oficialmente pela NLU. */
1221
+ type NluLanguage = 'pt' | 'en' | 'unknown';
1222
+ /**
1223
+ * Resultado da detecção — idioma + contagem de hits por dict pra debug.
1224
+ */
1225
+ interface LanguageDetectResult {
1226
+ readonly language: NluLanguage;
1227
+ readonly hits: {
1228
+ readonly pt: number;
1229
+ readonly en: number;
1230
+ };
1231
+ }
1232
+ /**
1233
+ * Detecta idioma dominante de uma sequência de tokens (já
1234
+ * tokenizados via `tokenize()`).
1235
+ *
1236
+ * **Retorna `'unknown'`** quando:
1237
+ * - Zero tokens
1238
+ * - Zero hits em ambos os idiomas (texto só de números/hex/símbolos)
1239
+ * - Empate exato (igual número de hits PT e EN)
1240
+ *
1241
+ * Caller decide o fallback (default PT? Persistido na UI?).
1242
+ */
1243
+ declare function detectLanguage(tokens: readonly string[]): LanguageDetectResult;
1244
+
1245
+ /**
1246
+ * **Number words — PT + EN.**
1247
+ *
1248
+ * Mapeia palavras de número escritas por extenso (PT/EN) para valor
1249
+ * numérico inteiro. Usado pelo `parseNumberToken` no slot-extractor
1250
+ * pra reconhecer comandos como "selecionar os três triangulos" →
1251
+ * count=3.
1252
+ *
1253
+ * **Cobertura**: 1-20 + dezenas (30, 40, ..., 100). Acima disso é
1254
+ * raro em comandos de editor SVG ("selecionar os 50 retangulos" é
1255
+ * mais natural com dígito).
1256
+ *
1257
+ * **Convenção de chave**: lowercase + SEM acento (tokenizer faz
1258
+ * `deaccent()`). Tres → `tres`, dois → `dois`, etc.
1259
+ */
1260
+ declare const NUMBER_WORDS: Readonly<Record<string, number>>;
1261
+ /**
1262
+ * Resolve uma palavra de número (lowercased, deacentuada) pro valor
1263
+ * inteiro. Retorna `null` quando não é número conhecido.
1264
+ */
1265
+ declare function resolveNumberWord(word: string): number | null;
1266
+
1267
+ /**
1268
+ * **`NluDictionaryRegistry`** — D-046 review-10 (Sprint 2.B / H5).
1269
+ *
1270
+ * Service injetável que permite consumers ESTENDEREM o vocabulário NLU
1271
+ * em runtime, sem editar os arquivos source dos dicts built-in
1272
+ * (`colors-*.ts`, `shapes-*.ts`, `actions-*.ts`).
1273
+ *
1274
+ * **Motivação**: aplicações de domínio (fluxograma, BPMN, UML, mapas)
1275
+ * têm vocabulário próprio ("swimlane", "decision", "actor", "use-case").
1276
+ * Sem este registry, a única forma de cobrir é editar source ou
1277
+ * registrar 20+ intents customizados duplicados.
1278
+ *
1279
+ * **API**:
1280
+ * - `registerColor(name, hex)` — adiciona "vibrant-blue" → "#1e90ff"
1281
+ * - `registerShape(name, kind)` — adiciona "actor" → 'circle' alias
1282
+ * - `registerAction(word, canonical)` — adiciona "compor" → 'create'
1283
+ * - `resolveColor` / `resolveShape` / `resolveAction` — consultam
1284
+ * dinâmico ANTES dos built-in (override permitido)
1285
+ *
1286
+ * **Multi-editor (D-042)**: singleton root-provided, mas extensions
1287
+ * são compartilhadas entre todos os editors. Plugins por-editor
1288
+ * podem usar `track(disposable)` pra cleanup.
1289
+ *
1290
+ * **Estado atual** (Fase 1): este registry é OPCIONAL. O parser
1291
+ * (`slot-extractor`, `menu-intent-discovery`) ainda consulta dicts
1292
+ * estáticos diretamente. Integração com este registry fica como
1293
+ * Sprint futuro (refactor de `resolveColorName` pra usar override
1294
+ * dinâmico, etc).
1295
+ */
1296
+ declare class NluDictionaryRegistry {
1297
+ private readonly _colors;
1298
+ private readonly _shapes;
1299
+ private readonly _actions;
1300
+ /** Snapshot reativo das cores customizadas. */
1301
+ readonly colors: Signal<ReadonlyMap<string, string>>;
1302
+ /** Snapshot reativo das shapes customizadas. */
1303
+ readonly shapes: Signal<ReadonlyMap<string, NluShapeKind>>;
1304
+ /** Snapshot reativo das actions customizadas. */
1305
+ readonly actions: Signal<ReadonlyMap<string, ActionCanonical>>;
1306
+ /**
1307
+ * Adiciona uma entrada de cor customizada. Retorna `Disposable` pra
1308
+ * remover (em geral trackeada por plugin via `ctx.track()`).
1309
+ *
1310
+ * @param name nome normalizado (lowercase, sem acento). Conflitos
1311
+ * com built-in fazem **override** silencioso (consumer tem prioridade).
1312
+ * @param hex CSS color (`#rrggbb`, `rgb(...)`, keyword) ou `'none'`/`'transparent'`.
1313
+ */
1314
+ registerColor(name: string, hex: string): Disposable;
1315
+ /**
1316
+ * Adiciona uma entrada de shape customizada (alias semântico).
1317
+ *
1318
+ * @param name nome normalizado.
1319
+ * @param kind {@link NluShapeKind} canonical.
1320
+ */
1321
+ registerShape(name: string, kind: NluShapeKind): Disposable;
1322
+ /**
1323
+ * Adiciona uma palavra de ação customizada → canonical existente.
1324
+ *
1325
+ * @param word palavra normalizada.
1326
+ * @param canonical {@link ActionCanonical} existente.
1327
+ */
1328
+ registerAction(word: string, canonical: ActionCanonical): Disposable;
1329
+ /**
1330
+ * Resolve cor consultando PRIMEIRO o registry dinâmico (consumer
1331
+ * tem prioridade), DEPOIS o dict estático built-in. Retorna `null`
1332
+ * quando não encontra.
1333
+ */
1334
+ resolveColor(name: string): string | null;
1335
+ /** Resolve shape no registry dinâmico (null se não encontrar). */
1336
+ resolveShape(name: string): NluShapeKind | null;
1337
+ /** Resolve action canonical no registry dinâmico (null se não). */
1338
+ resolveAction(word: string): ActionCanonical | null;
1339
+ /**
1340
+ * Helper genérico — registra entrada com validação básica e retorna
1341
+ * Disposable. Não-throw em conflito (override silencioso).
1342
+ */
1343
+ private registerEntry;
1344
+ static ɵfac: i0.ɵɵFactoryDeclaration<NluDictionaryRegistry, never>;
1345
+ static ɵprov: i0.ɵɵInjectableDeclaration<NluDictionaryRegistry>;
1346
+ }
1347
+
1348
+ /**
1349
+ * **Contrato de voz desacoplado** — D-046 voz híbrida.
1350
+ *
1351
+ * Define a abstração `VoiceProvider` num lugar **headless** (sem
1352
+ * Material, sem transformers.js) para que tanto a camada de UI
1353
+ * (`svg-engine/ai/nlu-ui`, provider Web Speech) quanto a camada WASM
1354
+ * (`svg-engine/ai/nlu-voice-wasm`, provider Whisper local) possam
1355
+ * compartilhar o mesmo contrato **sem dependência cruzada** entre elas.
1356
+ *
1357
+ * O orquestrador (`VoiceEngineService`, em `nlu-ui`) injeta o provider
1358
+ * Web Speech diretamente e o provider Whisper **opcionalmente** via
1359
+ * {@link VOICE_WHISPER_PROVIDER} — registrado pelo app só quando a voz
1360
+ * local é desejada (`provideWhisperVoiceEngine()`).
1361
+ */
1362
+ /**
1363
+ * Engine de reconhecimento de voz escolhível pelo usuário:
1364
+ * - `'web-speech'` — Web Speech API nativa do navegador (rápida, mas
1365
+ * depende de STT em nuvem do vendor — pode falhar com `network`).
1366
+ * - `'whisper'` — Whisper local via WASM (100% offline, sem rede).
1367
+ * - `'auto'` — tenta Web Speech e, em falha, cai para o Whisper.
1368
+ */
1369
+ type VoiceEngine = 'web-speech' | 'whisper' | 'auto';
1370
+ /**
1371
+ * Surface mínima comum de um provider de reconhecimento de voz.
1372
+ * Tanto `VoiceRecognitionService` (Web Speech) quanto
1373
+ * `WhisperVoiceService` (WASM) a satisfazem **estruturalmente** — não
1374
+ * é preciso `implements` nominal.
1375
+ */
1376
+ interface VoiceProvider {
1377
+ /** `false` quando o ambiente não suporta esta engine. */
1378
+ readonly isSupported: Signal<boolean>;
1379
+ /** `true` enquanto captura/processa. */
1380
+ readonly listening: Signal<boolean>;
1381
+ /** Último código de erro (`'network'`, `'not-allowed'`, …) ou `null`. */
1382
+ readonly lastError: Signal<string | null>;
1383
+ /** `true` durante carregamento de modelo (só engines com modelo, ex. Whisper). */
1384
+ readonly modelLoading?: Signal<boolean>;
1385
+ /** Inicia a captura; resolve com a transcrição final. */
1386
+ listen(lang?: string, options?: {
1387
+ readonly timeoutMs?: number;
1388
+ }): Promise<string>;
1389
+ /** Encerra a captura ativa, se houver. */
1390
+ stop(): void;
1391
+ }
1392
+ /**
1393
+ * Token DI **opcional** do provider Whisper local. Default `null`
1394
+ * (voz local não instalada). Apps que querem o Whisper offline
1395
+ * registram via `provideWhisperVoiceEngine()` de
1396
+ * `svg-engine/ai/nlu-voice-wasm`.
1397
+ */
1398
+ declare const VOICE_WHISPER_PROVIDER: InjectionToken<VoiceProvider | null>;
1399
+
1400
+ /**
1401
+ * **D-093 — contrato de provedor LLM (chat) desacoplado.**
1402
+ *
1403
+ * Define a abstração `AiChatProvider` num lugar **headless** (sem rede
1404
+ * amarrada, sem Material) — exatamente como o {@link VoiceProvider} fez
1405
+ * para voz. Qualquer backend (Ollama local, OpenAI, Anthropic, vLLM…)
1406
+ * que satisfaça este contrato pode alimentar o {@link LlmIntentResolverService}
1407
+ * sem que o resolver saiba qual é.
1408
+ *
1409
+ * O resolver injeta o provider **opcionalmente** via {@link AI_CHAT_PROVIDER}
1410
+ * (default `null` — LLM não instalado). Apps que querem a camada LLM
1411
+ * registram um provider concreto (ex.: `provideOllamaChat(...)`).
1412
+ */
1413
+ /** Papel de uma mensagem no diálogo (formato estilo chat completions). */
1414
+ type AiChatRole = 'system' | 'user' | 'assistant';
1415
+ /** Uma mensagem do diálogo enviada ao modelo. */
1416
+ interface AiChatMessage {
1417
+ readonly role: AiChatRole;
1418
+ readonly content: string;
1419
+ }
1420
+ /**
1421
+ * Opções por-chamada. Tudo opcional — o provider aplica seus defaults.
1422
+ *
1423
+ * **`model`** é o ponto-chave do requisito do usuário: permite **trocar
1424
+ * o modelo por requisição conforme a complexidade** do conteúdo (ex.:
1425
+ * `qwen2.5:3b` para o trivial, `qwen2.5:7b` para composições pesadas)
1426
+ * sem reconfigurar o provider.
1427
+ */
1428
+ interface AiChatOptions {
1429
+ /** Sobrescreve o modelo do provider só nesta chamada (roteamento por complexidade). */
1430
+ readonly model?: string;
1431
+ /** Pede saída **JSON** estruturada (mapeia para `format:"json"` no Ollama). */
1432
+ readonly format?: 'json';
1433
+ /** Temperatura de amostragem (0 = determinístico). */
1434
+ readonly temperature?: number;
1435
+ /** Teto de tokens gerados (mapeia para `num_predict` no Ollama). */
1436
+ readonly maxTokens?: number;
1437
+ /** Cancela a chamada (timeout / troca de contexto). */
1438
+ readonly signal?: AbortSignal;
1439
+ }
1440
+ /**
1441
+ * Surface mínima de um provedor de chat LLM. Satisfeito
1442
+ * **estruturalmente** — não exige `implements` nominal.
1443
+ */
1444
+ interface AiChatProvider {
1445
+ /** `false` quando o provider não está utilizável (sem baseUrl, etc.). */
1446
+ readonly isConfigured: Signal<boolean>;
1447
+ /** Modelo default deste provider (o usado quando `opts.model` é omitido). */
1448
+ readonly defaultModel: Signal<string>;
1449
+ /**
1450
+ * **D-095 — modelos sugeridos** (curados/conhecidos), opcional. A UI funde
1451
+ * esta lista com os modelos descobertos ao vivo ({@link
1452
+ * AiChatProvider.listModels}) para popular o seletor — útil de fallback
1453
+ * quando a descoberta falha e como sugestão para consumidores. Backends sem
1454
+ * curadoria simplesmente não expõem (a UI cai nos descobertos + default).
1455
+ */
1456
+ readonly suggestedModels?: Signal<readonly string[]>;
1457
+ /**
1458
+ * Envia o diálogo e resolve com o **texto** da resposta do assistente.
1459
+ * Lança em erro de rede / HTTP — o chamador (resolver) trata.
1460
+ */
1461
+ chat(messages: readonly AiChatMessage[], opts?: AiChatOptions): Promise<string>;
1462
+ /**
1463
+ * **D-094 — descoberta de modelos** (opcional). Lista os modelos
1464
+ * disponíveis no backend para o usuário escolher (ex.: Ollama
1465
+ * `GET /api/tags`). O contrato é opcional: backends que não expõem um
1466
+ * catálogo (ou que só servem um modelo) simplesmente não implementam, e
1467
+ * a UI cai no {@link AiChatProvider.defaultModel}. Lança em erro de rede
1468
+ * / HTTP — o chamador trata e degrada para o default.
1469
+ */
1470
+ listModels?(): Promise<readonly string[]>;
1471
+ }
1472
+ /**
1473
+ * Token DI **opcional** do provedor LLM. Default `null` (camada LLM não
1474
+ * instalada → o {@link LlmIntentResolverService} reporta `isAvailable === false`
1475
+ * e o app continua só com o NLU rule-based). Apps registram um provider
1476
+ * concreto via, p.ex., `provideOllamaChat({ baseUrl, model })`.
1477
+ */
1478
+ declare const AI_CHAT_PROVIDER: InjectionToken<AiChatProvider | null>;
1479
+
1480
+ /** Default Ollama endpoint (local). Override via {@link provideOllamaChat}. */
1481
+ declare const DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434";
1482
+ /** Default model — the small/fast one that fits a modest GPU (D-093 benchmark). */
1483
+ declare const DEFAULT_OLLAMA_MODEL = "qwen2.5:3b";
1484
+ /**
1485
+ * **D-095 — modelos curados (conhecidos) sugeridos no seletor.**
1486
+ *
1487
+ * Lista de fallback/curadoria que o `<svge-nlu-input>` funde com os modelos
1488
+ * **descobertos** ao vivo (`/api/tags`): garante que esses apareçam no seletor
1489
+ * mesmo quando a descoberta falha (servidor offline/sem CORS) e serve de
1490
+ * sugestão para consumidores da lib que ainda não puxaram nada.
1491
+ *
1492
+ * Inclui os modelos base (D-093) **e** os **`qwen2.5-coder`** (3b/7b/14b) —
1493
+ * estes últimos são **coder-tuned**, logo bem melhores para gerar SVG (que é
1494
+ * markup/código): fecham tags, respeitam `viewBox`/`path`/`defs`. Mantidos os
1495
+ * anteriores; os coder são **adicionais** (vide D-094 modo "SVG livre").
1496
+ */
1497
+ declare const DEFAULT_OLLAMA_MODELS: readonly string[];
1498
+ /** Optional configuration for {@link OllamaChatProvider} / {@link provideOllamaChat}. */
1499
+ interface OllamaChatConfig {
1500
+ /** Base URL of the Ollama server, e.g. `http://192.168.1.21:11434`. */
1501
+ readonly baseUrl?: string;
1502
+ /** Default model id, e.g. `qwen2.5:3b`. Overridable per-call via `opts.model`. */
1503
+ readonly model?: string;
1504
+ /**
1505
+ * **D-095** — sobrescreve a lista curada de modelos sugeridos no seletor
1506
+ * ({@link DEFAULT_OLLAMA_MODELS}). Quando omitido, usa a curadoria padrão.
1507
+ */
1508
+ readonly models?: readonly string[];
1509
+ }
1510
+ /**
1511
+ * **D-093 — `AiChatProvider` para Ollama** (API nativa `/api/chat`).
1512
+ *
1513
+ * `fetch` puro contra um servidor Ollama. Sem dependência pesada (ao
1514
+ * contrário do Whisper), então mora no mesmo entry point `svg-engine/ai/nlu`
1515
+ * junto do contrato — o token {@link AI_CHAT_PROVIDER} mantém a troca de
1516
+ * backend desacoplada.
1517
+ *
1518
+ * **baseUrl/model são signals settáveis em runtime** — atende o requisito
1519
+ * de "trocar provider/model conforme a complexidade": o app pode chamar
1520
+ * `setModel('qwen2.5:7b')` para conteúdo pesado, ou passar `opts.model`
1521
+ * por chamada (sem mexer no default).
1522
+ *
1523
+ * **CORS**: o servidor Ollama precisa subir com `OLLAMA_ORIGINS` liberando
1524
+ * a origem do app (validado no D-093). Erros de rede/HTTP são propagados
1525
+ * via `throw` — o {@link LlmIntentResolverService} captura e degrada.
1526
+ */
1527
+ declare class OllamaChatProvider implements AiChatProvider {
1528
+ private readonly _baseUrl;
1529
+ private readonly _model;
1530
+ private readonly _suggestedModels;
1531
+ /** Current base URL (reactive). */
1532
+ readonly baseUrl: i0.Signal<string>;
1533
+ /** Current default model (reactive). Satisfies {@link AiChatProvider.defaultModel}. */
1534
+ readonly defaultModel: i0.Signal<string>;
1535
+ /**
1536
+ * **D-095** — modelos curados sugeridos no seletor (reactive). Satisfaz
1537
+ * {@link AiChatProvider.suggestedModels}. Fundidos com os descobertos via
1538
+ * `/api/tags` pela UI; default = {@link DEFAULT_OLLAMA_MODELS}.
1539
+ */
1540
+ readonly suggestedModels: i0.Signal<readonly string[]>;
1541
+ /** `true` once a non-empty base URL is set (it always is, by default). */
1542
+ readonly isConfigured: i0.Signal<boolean>;
1543
+ /** Apply a partial config (only the provided fields change). */
1544
+ configure(cfg: OllamaChatConfig): void;
1545
+ /** Switch the server URL at runtime. */
1546
+ setBaseUrl(url: string): void;
1547
+ /** Switch the default model at runtime (complexity routing). */
1548
+ setModel(model: string): void;
1549
+ chat(messages: readonly AiChatMessage[], opts?: AiChatOptions): Promise<string>;
1550
+ /**
1551
+ * **D-094** — lista os modelos instalados no servidor Ollama via
1552
+ * `GET /api/tags`. Alimenta o seletor de modelo da UI (o usuário escolhe
1553
+ * entre os modelos disponíveis em runtime). Devolve os nomes (`name`,
1554
+ * ex.: `qwen2.5:3b`) ordenados alfabeticamente e deduplicados. Propaga
1555
+ * erro de rede / HTTP — o chamador degrada para o {@link defaultModel}.
1556
+ */
1557
+ listModels(): Promise<readonly string[]>;
1558
+ static ɵfac: i0.ɵɵFactoryDeclaration<OllamaChatProvider, never>;
1559
+ static ɵprov: i0.ɵɵInjectableDeclaration<OllamaChatProvider>;
1560
+ }
1561
+ /**
1562
+ * **D-093** — DI helper that wires {@link OllamaChatProvider} as the active
1563
+ * {@link AI_CHAT_PROVIDER}. Add to an app/route `providers: []`:
1564
+ *
1565
+ * ```ts
1566
+ * providers: [
1567
+ * provideOllamaChat({ baseUrl: 'http://192.168.1.21:11434', model: 'qwen2.5:3b' }),
1568
+ * ]
1569
+ * ```
1570
+ *
1571
+ * The same instance is reachable as both {@link OllamaChatProvider} (for
1572
+ * runtime `setModel`/`setBaseUrl`) and {@link AI_CHAT_PROVIDER} (what the
1573
+ * resolver consumes).
1574
+ */
1575
+ declare function provideOllamaChat(cfg?: OllamaChatConfig): Provider[];
1576
+
1577
+ /**
1578
+ * **D-093 Fase 4** — teto de entradas no catálogo enviado ao modelo.
1579
+ *
1580
+ * Descoberta empírica (teste ao vivo 3b E 7b): com os 222 intents
1581
+ * registrados o prompt chega a ~4095 tokens — **~74s só de ingestão**
1582
+ * nessa GPU — e o modelo **perde o contrato de saída** (devolve um JSON
1583
+ * `{"card":{…}}` inventado em vez de `{"steps":[…]}`). Curar o catálogo
1584
+ * para um subconjunto relevante corta a latência E ajuda o modelo a
1585
+ * ancorar no formato. 24 cobre create-shape + cor/texto + os intents
1586
+ * textualmente relacionados ao pedido com folga.
1587
+ */
1588
+ declare const DEFAULT_CATALOG_MAX_ENTRIES = 24;
1589
+ /**
1590
+ * **D-093 Fase 4** — intents **sempre** mantidos no catálogo curado
1591
+ * (casados por `id.includes(hint)`). São as primitivas de composição:
1592
+ * sem `create-shape` o modelo não tem como montar um "card de KPI" a
1593
+ * partir do zero. Curtos de propósito; ampliar só com primitiva nova.
1594
+ */
1595
+ declare const CORE_INTENT_ID_HINTS: readonly string[];
1596
+ /**
1597
+ * Compact catalog entry fed to the model — one registered intent reduced
1598
+ * to id + description + slot names/kinds. Kept small on purpose (token
1599
+ * budget; the model only needs to pick an id and fill slots).
1600
+ */
1601
+ interface LlmIntentCatalogEntry {
1602
+ readonly id: string;
1603
+ readonly description: string;
1604
+ /** `{ slotName: kind }`, e.g. `{ fill: 'color', width: 'number' }`. */
1605
+ readonly slots: Record<string, string>;
1606
+ }
1607
+ /** One resolved (validated) step of a plan — intent guaranteed to exist. */
1608
+ interface LlmResolvedStep {
1609
+ readonly intentId: string;
1610
+ readonly intent: NluIntent;
1611
+ readonly slots: Record<string, unknown>;
1612
+ }
1613
+ /** The validated plan returned by {@link LlmIntentResolverService.resolvePlan}. */
1614
+ interface LlmResolvedPlan {
1615
+ /** Steps whose `intentId` matched a registered intent (in order). */
1616
+ readonly steps: readonly LlmResolvedStep[];
1617
+ /** Model-reported confidence in `[0,1]` (defaults to 1 if absent). */
1618
+ readonly confidence: number;
1619
+ /** Raw model text (for debugging / UI "show reasoning"). */
1620
+ readonly raw: string;
1621
+ /** `intentId`s the model produced that do NOT exist (dropped, for telemetry). */
1622
+ readonly dropped: readonly string[];
1623
+ }
1624
+ /** Per-call options for the resolver. */
1625
+ interface LlmResolveOptions {
1626
+ /** Model override for THIS request (complexity routing: 3b vs 7b). */
1627
+ readonly model?: string;
1628
+ /** Abort the underlying request. */
1629
+ readonly signal?: AbortSignal;
1630
+ /** Cap generated tokens. */
1631
+ readonly maxTokens?: number;
1632
+ }
1633
+ /** Options for {@link LlmIntentResolverService.resolveAndExecute}. */
1634
+ interface LlmExecuteOptions extends LlmResolveOptions {
1635
+ /** Confirmation gate forwarded to {@link NaturalLanguageService.executeCandidate} (required for destructive steps). */
1636
+ readonly confirmGate?: NluExecuteOptions['confirmGate'];
1637
+ }
1638
+ /**
1639
+ * **D-094 — modo SEM catálogo.** Resultado de {@link
1640
+ * LlmIntentResolverService.generateSvg}: o SVG completo extraído da resposta
1641
+ * do modelo + o texto cru (debug / "ver resposta").
1642
+ */
1643
+ interface LlmRawSvgResult {
1644
+ /** Markup `<svg>…</svg>` extraído (fences/prosa removidos). `''` se nenhum. */
1645
+ readonly svg: string;
1646
+ /** Texto cru retornado pelo modelo (para debug / UI). */
1647
+ readonly raw: string;
1648
+ }
1649
+ /**
1650
+ * **D-094 — modo SEM catálogo.** Resultado de {@link
1651
+ * LlmIntentResolverService.generateAndInsertSvg}: o SVG gerado + o desfecho
1652
+ * da inserção no canvas (id do nó inserido ou um erro amigável).
1653
+ */
1654
+ interface LlmRawSvgInsertResult {
1655
+ /** `true` quando o SVG foi parseado e inserido no documento. */
1656
+ readonly ok: boolean;
1657
+ /** Markup SVG gerado (mesmo quando a inserção falhou — para debug). */
1658
+ readonly svg: string;
1659
+ /** Texto cru do modelo. */
1660
+ readonly raw: string;
1661
+ /** Id do nó inserido (presente só quando `ok`). */
1662
+ readonly nodeId?: NodeId;
1663
+ /** Avisos de saneamento do importador (scripts/handlers removidos, etc.). */
1664
+ readonly warnings: readonly string[];
1665
+ /** Mensagem de erro amigável quando `ok` é `false`. */
1666
+ readonly error?: string;
1667
+ }
1668
+ /**
1669
+ * **D-093 — `LlmIntentResolverService`** (resolver de intents via LLM).
1670
+ *
1671
+ * O **fallback inteligente** do NLU: quando o rule-based não resolve, o
1672
+ * texto livre é mandado ao LLM ({@link AI_CHAT_PROVIDER}), que devolve um
1673
+ * **plano** de comandos **já registrados** (intentId + slots). Cada passo
1674
+ * é validado contra o registry e executado pelo pipeline seguro
1675
+ * ({@link NaturalLanguageService.executeCandidate} — gate de destrutivos,
1676
+ * try/catch). **O LLM nunca inventa comando** — só escolhe dos existentes,
1677
+ * o que mantém a segurança.
1678
+ *
1679
+ * **Opcional por design**: se nenhum {@link AI_CHAT_PROVIDER} foi
1680
+ * registrado, `isAvailable === false` e o app segue só com o rule-based
1681
+ * (zero rede, specs offline). Root-scoped como o {@link NaturalLanguageService}
1682
+ * (registry global; execução per-scope via `NluContext.injector`).
1683
+ */
1684
+ declare class LlmIntentResolverService {
1685
+ private readonly nlu;
1686
+ private readonly provider;
1687
+ /** `true` quando um provider LLM foi registrado (camada disponível). */
1688
+ get isAvailable(): boolean;
1689
+ /**
1690
+ * **D-094** — modelo default do provider (o usado quando nenhum override
1691
+ * é passado). `null` quando não há provider. Serve de fallback ao seletor
1692
+ * de modelo da UI quando o backend não lista modelos.
1693
+ */
1694
+ get defaultModel(): string | null;
1695
+ /**
1696
+ * **D-094** — lista os modelos disponíveis no backend para o seletor de
1697
+ * modelo da UI. Quando o provider não implementa descoberta
1698
+ * ({@link AiChatProvider.listModels} opcional) ou não há provider,
1699
+ * devolve `[]` — a UI cai no {@link defaultModel}. Erros de rede são
1700
+ * propagados para o chamador decidir como degradar.
1701
+ */
1702
+ listModels(): Promise<readonly string[]>;
1703
+ /**
1704
+ * **D-095** — modelos sugeridos (curados/conhecidos) do provider, para a UI
1705
+ * fundir com os descobertos ({@link listModels}). `[]` quando não há
1706
+ * provider ou ele não expõe curadoria ({@link AiChatProvider.suggestedModels}
1707
+ * é opcional). Síncrono (apenas lê um signal, sem rede).
1708
+ */
1709
+ suggestedModels(): readonly string[];
1710
+ /**
1711
+ * Catálogo compacto dos intents registrados — enviado ao modelo no
1712
+ * system prompt. Deriva de `NaturalLanguageService.intents()`.
1713
+ *
1714
+ * **D-093 Fase 4 — curadoria por relevância**: quando há `text` E o
1715
+ * total de intents excede `maxEntries`, o catálogo é **pré-filtrado**
1716
+ * para um subconjunto relevante (primitivas core + top-K por
1717
+ * sobreposição de tokens com o pedido). Sem `text` — ou quando o
1718
+ * registry já cabe em `maxEntries` — devolve TODOS (comportamento
1719
+ * legado, specs offline intactos). Isso corta o prompt de ~4095 →
1720
+ * algumas centenas de tokens (latência) E ajuda o modelo a ancorar
1721
+ * no contrato de saída (vide nota de {@link DEFAULT_CATALOG_MAX_ENTRIES}).
1722
+ *
1723
+ * @param text pedido do usuário (opcional) — base da relevância.
1724
+ * @param opts `maxEntries` para sobrescrever o teto padrão.
1725
+ */
1726
+ buildCatalog(text?: string, opts?: {
1727
+ maxEntries?: number;
1728
+ }): readonly LlmIntentCatalogEntry[];
1729
+ /**
1730
+ * Pede um plano ao LLM e valida cada passo contra o registry.
1731
+ * @throws se nenhum provider estiver registrado, ou se o modelo não
1732
+ * produzir JSON parseável.
1733
+ */
1734
+ resolvePlan(text: string, opts?: LlmResolveOptions): Promise<LlmResolvedPlan>;
1735
+ /**
1736
+ * Resolve o plano e **executa** cada passo pelo pipeline seguro do NLU.
1737
+ * Passos não-destrutivos rodam direto; destrutivos exigem `confirmGate`
1738
+ * (senão são rejeitados, como em `executeCandidate`). Cada passo vira um
1739
+ * comando próprio no bus → **um passo de undo por etapa**.
1740
+ */
1741
+ resolveAndExecute(text: string, ctx: NluContext, opts?: LlmExecuteOptions): Promise<readonly NluExecuteResult[]>;
1742
+ /**
1743
+ * **D-094 — modo SEM catálogo.** Pede ao LLM um **SVG completo** (sem
1744
+ * catálogo de intents, sem plano de passos) e extrai o `<svg>…</svg>` da
1745
+ * resposta. Ao contrário de {@link resolvePlan}, **não** força
1746
+ * `format:"json"` — a saída é markup SVG/XML cru. O `temperature:0` mantém
1747
+ * o resultado determinístico. O chamador trata erro de rede / SVG ausente.
1748
+ *
1749
+ * @throws se nenhum provider estiver registrado.
1750
+ */
1751
+ generateSvg(text: string, opts?: LlmResolveOptions): Promise<LlmRawSvgResult>;
1752
+ /**
1753
+ * **D-094 — modo SEM catálogo.** Gera o SVG via {@link generateSvg},
1754
+ * parseia/saneia com o `svgImporter` (remove `<script>`, `on*`,
1755
+ * `javascript:` hrefs) e o **desenha no canvas** aditivamente, centralizado
1756
+ * na página ativa — exatamente o pipeline de `File ▸ Import ▸ SVG`
1757
+ * ({@link ImportPlacementService.placeDocumentCentered}, resolvido do
1758
+ * **escopo do editor** via `ctx.injector`). Nunca lança por SVG inválido:
1759
+ * devolve `{ ok:false, error }` para a UI mostrar.
1760
+ */
1761
+ generateAndInsertSvg(text: string, ctx: NluContext, opts?: LlmResolveOptions): Promise<LlmRawSvgInsertResult>;
1762
+ static ɵfac: i0.ɵɵFactoryDeclaration<LlmIntentResolverService, never>;
1763
+ static ɵprov: i0.ɵɵInjectableDeclaration<LlmIntentResolverService>;
1764
+ }
1765
+ /** Raw (pre-validation) step shape produced by the model. */
1766
+ interface RawPlanStep {
1767
+ readonly intentId: string;
1768
+ readonly slots: Record<string, unknown>;
1769
+ }
1770
+ /**
1771
+ * Tolerant parse of the model's JSON. Strips markdown fences, slices to
1772
+ * the outermost `{...}`, and normalizes several shapes (top-level array,
1773
+ * single step, `{steps:[...]}`). Throws only when nothing parses.
1774
+ */
1775
+ declare function parsePlan(raw: string): {
1776
+ steps: RawPlanStep[];
1777
+ confidence: number;
1778
+ };
1779
+ /**
1780
+ * **D-094** — extract the `<svg>…</svg>` element from a model reply. Strips
1781
+ * markdown fences (```svg / ```xml / ```html / bare ```), then slices from the
1782
+ * first `<svg` tag to the last `</svg>`. Returns `''` when no SVG is present
1783
+ * (the caller surfaces a friendly "no SVG returned" error). Tolerant of prose
1784
+ * before/after, which small models sometimes emit despite the instruction.
1785
+ */
1786
+ declare function extractSvgBlob(raw: string): string;
1787
+
1788
+ /**
1789
+ * **D-093 (Fase 3)** — gatilho de escalonamento para o LLM.
1790
+ *
1791
+ * Problema (descoberto na Fase 2): o rule-based é **guloso** com verbos
1792
+ * de criação — "crie um card de KPI moderno com título e valor" casa
1793
+ * `create-shape` com 90% (gera 1 retângulo default) e **não** escala,
1794
+ * então pedidos complexos nunca chegam ao LLM.
1795
+ *
1796
+ * Heurística aqui: o pedido é **vago para o rule-based** quando tem
1797
+ * conteúdo suficiente mas a maior parte dele **não é reconhecida** pelos
1798
+ * dicionários (não é forma, cor, número/dimensão nem verbo de ação).
1799
+ * Nesse caso o `<svge-nlu-input>` roteia direto para o LLM — que usa o
1800
+ * texto inteiro — em vez de deixar o rule-based criar uma forma genérica.
1801
+ *
1802
+ * Protege os bons casos:
1803
+ * - "criar retângulo vermelho 100x50" → 4/4 reconhecidos → NÃO vago.
1804
+ * - "undo", "selecionar tudo" → poucos tokens → NÃO vago.
1805
+ * - "crie um card de KPI moderno com título e valor" → só "crie"
1806
+ * reconhecido (1/6) → **vago** → LLM.
1807
+ */
1808
+ /** Mínimo de tokens significativos para considerar o roteamento (frases curtas ficam no rule-based). */
1809
+ declare const VAGUE_MIN_MEANINGFUL_TOKENS = 3;
1810
+ /** Abaixo desta fração de tokens reconhecidos, o pedido é "vago". */
1811
+ declare const VAGUE_RECOGNIZED_FRACTION = 0.5;
1812
+ /**
1813
+ * Substantivos **compostos** — coisas feitas de várias primitivas. Quando
1814
+ * aparecem, o rule-based reduz tudo a UMA forma genérica (o `create-shape`
1815
+ * casa "card"/"kpi" como alias de forma e gera 1 retângulo), então
1816
+ * roteamos direto ao LLM, que sabe decompor em vários comandos.
1817
+ * Deaccentuados/lowercase (forma do tokenizer).
1818
+ */
1819
+ declare const COMPOSITE_KEYWORDS: ReadonlySet<string>;
1820
+ /**
1821
+ * Decide se `text` deve pular o rule-based e ir direto ao LLM.
1822
+ * Pure function — sem efeitos colaterais, testável offline.
1823
+ *
1824
+ * Vago quando QUALQUER:
1825
+ * 1. contém um substantivo composto ({@link COMPOSITE_KEYWORDS}) — ex.:
1826
+ * "card", "dashboard", "organograma" (o rule-based só faria 1 forma); OU
1827
+ * 2. tem conteúdo suficiente mas a maioria dos tokens **não é reconhecida**
1828
+ * pelos dicionários (forma/cor/número/ação) — ex.: "casa com telhado e
1829
+ * porta".
1830
+ */
1831
+ declare function isVagueForRuleBased(text: string): boolean;
1832
+
1833
+ export { ACTION_DICTIONARY, ACTION_DICTIONARY_EN, ACTION_DICTIONARY_PT, ACTION_KEYS, AI_CHAT_PROVIDER, COLOR_DICTIONARY, COLOR_DICTIONARY_EN, COLOR_DICTIONARY_PT, COLOR_KEYS, COMPOSITE_KEYWORDS, CORE_INTENT_ID_HINTS, DEFAULT_CATALOG_MAX_ENTRIES, DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_MODEL, DEFAULT_OLLAMA_MODELS, HEX_COLOR_RE, LIGHTNESS_MODIFIERS, LIGHTNESS_MULTIPLIERS, LlmIntentResolverService, NUMBER_WORDS, NaturalLanguageService, NluDictionaryRegistry, OllamaChatProvider, SHAPE_DICTIONARY, SHAPE_DICTIONARY_EN, SHAPE_DICTIONARY_PT, SHAPE_KEYS, STOPWORDS, STOPWORDS_EN, STOPWORDS_PT, VAGUE_MIN_MEANINGFUL_TOKENS, VAGUE_RECOGNIZED_FRACTION, VOICE_WHISPER_PROVIDER, adaptiveMaxDistance, adjustHexLightness, bestMatch, builtinNluPlugin, darkenHex, deaccent, detectLanguage, discoverMenuIntents, discoverMenuIntentsReactive, extractSlots, extractSvgBlob, fuzzyMatchAll, fuzzyMatchAny, fuzzyMatchToken, hexToRgb, hslToRgb, isStopword, isVagueForRuleBased, levenshtein, lightenHex, menuContributionToIntent, normalize, parseColorPhrase, parseColorToken, parseDimensionToken, parseHslFunction, parseNumberToken, parsePlan, parseRgbFunction, provideOllamaChat, resolveActionCanonical, resolveColorName, resolveNumberWord, resolveShapeKind, rgbToHsl, tokenize, tokenizeWithoutStopwords };
1834
+ export type { ActionCanonical, AiChatMessage, AiChatOptions, AiChatProvider, AiChatRole, ColorPhraseMatch, DiscoverMenuIntentsReactiveResult, DiscoverMenuIntentsResult, ExtractContext, ExtractedSlots, FuzzyMatch, LanguageDetectResult, LlmExecuteOptions, LlmIntentCatalogEntry, LlmRawSvgInsertResult, LlmRawSvgResult, LlmResolveOptions, LlmResolvedPlan, LlmResolvedStep, NluCandidate, NluContext, NluExecuteOptions, NluExecuteResult, NluIntent, NluLanguage, NluMatchReason, NluParseOptions, NluShapeKind, NluSlotSchema, OllamaChatConfig, VoiceEngine, VoiceProvider };